return gui.AddControl(gui_control_type, aParam3, aParam4); // It already displayed any error.
case GUI_CMD_MARGIN:
if (*aParam2)
gui.mMarginX = ATOI(aParam2); // Seems okay to allow negative margins.
if (*aParam3)
gui.mMarginY = ATOI(aParam3); // Seems okay to allow negative margins.
return OK;
case GUI_CMD_MENU:
UserMenu *menu;
if (*aParam2)
{
// By design, the below will give a slightly misleading error if the specified menu is the
// TRAY menu, since it should be obvious that it cannot be used as a menu bar (since it
// must always be of the popup type):
if ( !(menu = FindMenu(aParam2)) || menu == g_script.mTrayMenu ) // Relies on short-circuit boolean.
return ScriptError(ERR_MENU ERR_ABORT, aParam2);
menu->Create(MENU_TYPE_BAR); // Ensure the menu physically exists and is the "non-popup" type (for a menu bar).
}
else
menu = NULL;
SetMenu(gui.mHwnd, menu ? menu->mMenu : NULL); // Add or remove the menu.
return OK;
case GUI_CMD_SHOW:
return gui.Show(aParam2, aParam3);
case GUI_CMD_SUBMIT:
return gui.Submit(stricmp(aParam2, "NoHide"));
case GUI_CMD_CANCEL:
return gui.Cancel();
case GUI_CMD_MINIMIZE:
// If the window is hidden, it is unhidden as a side-effect (this happens even for SW_SHOWMINNOACTIVE).
ShowWindow(gui.mHwnd, SW_MINIMIZE);
return OK;
case GUI_CMD_MAXIMIZE:
ShowWindow(gui.mHwnd, SW_MAXIMIZE); // If the window is hidden, it is unhidden as a side-effect.
return OK;
case GUI_CMD_RESTORE:
ShowWindow(gui.mHwnd, SW_RESTORE); // If the window is hidden, it is unhidden as a side-effect.
return OK;
case GUI_CMD_FONT:
return gui.SetCurrentFont(aParam2, aParam3);
case GUI_CMD_LISTVIEW:
case GUI_CMD_TREEVIEW:
if (*aParam2)
{
GuiIndexType control_index = gui.FindControl(aParam2); // Search on either the control's variable name or its ClassNN.
if (control_index != -1) // Must compare directly to -1 due to unsigned.
{
GuiControlType &control = gui.mControl[control_index]; // For maintainability, and might slightly reduce code size.
if (gui_command == GUI_CMD_LISTVIEW)
{
if (control.type == GUI_CONTROL_LISTVIEW) // v1.0.46.09: Must validate that it's the right type of control; otherwise some LV_* functions can crash due to the control not having malloc'd the special ListView struct that tracks column attributes.
gui.mCurrentListView = &control;
//else mismatched control type, so just leave it unchanged.
}
else // GUI_CMD_TREEVIEW
{
if (control.type == GUI_CONTROL_TREEVIEW)
gui.mCurrentTreeView = &control;
//else mismatched control type, so just leave it unchanged.
}
}
//else it seems best never to change ite to be "no control" since it doesn't seem to have much use.
if (*next_option == '*') // Options are present. Must check this here and in the for-loop to avoid omitting legitimate whitespace in a filename that starts with spaces.
{
char *option_end, orig_char;
for (; *next_option == '*'; next_option = omit_leading_whitespace(option_end))
{
// Find the end of this option item:
if ( !(option_end = StrChrAny(next_option, " \t")) ) // Space or tab.
option_end = next_option + strlen(next_option); // Set to position of zero terminator instead.
// Permanently terminate in between options to help eliminate ambiguity for words contained
// inside other words, and increase confidence in decimal and hexadecimal conversion.
orig_char = *option_end;
*option_end = '\0';
++next_option; // Skip over the asterisk. It might point to a zero terminator now.
// DTS_SHORTDATEFORMAT and DTS_SHORTDATECENTURYFORMAT
// seem to produce identical results (both display 4-digit year), at least on XP. Perhaps
// DTS_SHORTDATECENTURYFORMAT is obsolete. In any case, it's uncommon so for simplicity, is
// not a named style. It can always be applied numerically if desired. Update:
// DTS_SHORTDATECENTURYFORMAT is now applied by default upon creation, which can be overridden
// explicitly via -0x0C in the control's options.
if (!stricmp(aParam3, "LongDate")) // LongDate seems more readable than "Long". It also matches the keyword used by FormatTime.
style |= DTS_LONGDATEFORMAT; // Competing styles were already purged above.
else if (!stricmp(aParam3, "Time"))
style |= DTS_TIMEFORMAT; // Competing styles were already purged above.
else // Custom format.
use_custom_format = true;
}
//else aText is blank and use_custom_format==false, which will put DTS_SHORTDATEFORMAT into effect.
if (!use_custom_format)
SetWindowLong(control.hwnd, GWL_STYLE, style);
//else leave style unchanged so that if format is later removed, the underlying named style will
// not have been altered.
// This both adds and removes the custom format depending on aParma3:
DateTime_SetFormat(control.hwnd, use_custom_format ? aParam3 : NULL); // NULL removes any custom format so that the underlying style format is revealed.
}
return OK;
case GUI_CONTROL_MONTHCAL:
if (*aParam3)
{
DWORD gdtr = YYYYMMDDToSystemTime2(aParam3, st);
if (!gdtr) // Neither min nor max is present (or both are invalid).
break; // Leave current sel. unchanged.
if (GetWindowLong(control.hwnd, GWL_STYLE) & MCS_MULTISELECT) // Must use range-selection even if selection is only one date.
{
if (gdtr == GDTR_MIN) // No maximum is present, so set maximum to minimum.
st[1] = st[0];
//else just max, or both are present. Assume both for code simplicity.
if (guicontrolget_cmd == GUICONTROLGET_CMD_INVALID)
{
// This is caught at load-time 99% of the time and can only occur here if the sub-command name
// is contained in a variable reference. Since it's so rare, the handling of it is debatable,
// but to keep it simple just set ErrorLevel:
output_var.Assign(); // For backward-compatibility and also serves as an additional indicator of failure.
return g_ErrorLevel->Assign(ERRORLEVEL_ERROR);
}
else if (guicontrolget_cmd != GUICONTROLGET_CMD_POS) // v1.0.46.09: Avoid resetting the variable for the POS mode, since it uses and array and the user might want the existing contents of the GUI variable retained.
output_var.Assign(); // Set default to be blank for all commands except POS, for consistency.
if (window_index < 0 || window_index >= MAX_GUI_WINDOWS || !g_gui[window_index]) // Relies on short-circuit boolean order.
// This departs from the tradition used by PerformGui() but since this type of error is rare,
// and since use ErrorLevel adds a little bit of flexibility (since the script's curretn thread
// is not unconditionally aborted), this seems best:
return g_ErrorLevel->Assign(ERRORLEVEL_ERROR);
GuiType &gui = *g_gui[window_index]; // For performance and convenience.
if (!*aControlID) // In this case, default to the name of the output variable, as documented.
aControlID = output_var.mName;
// Beyond this point, errors are rare so set the default to "no error":
g_ErrorLevel->Assign(ERRORLEVEL_NONE);
// Handle GUICONTROLGET_CMD_FOCUS(V) early since it doesn't need a specified ControlID:
if (guicontrolget_cmd == GUICONTROLGET_CMD_FOCUS || guicontrolget_cmd == GUICONTROLGET_CMD_FOCUSV)
{
class_and_hwnd_type cah;
cah.hwnd = GetFocus();
GuiControlType *pcontrol;
if (!cah.hwnd || !(pcontrol = gui.FindControl(cah.hwnd))) // Relies on short-circuit boolean order.
return g_ErrorLevel->Assign(ERRORLEVEL_ERROR);
char focused_control[WINDOW_CLASS_SIZE];
if (guicontrolget_cmd == GUICONTROLGET_CMD_FOCUSV) // v1.0.43.06.
// GUI_HWND_TO_INDEX vs FindControl() is enough because FindControl() was alraedy called above:
// Rather than deal with the confusion of an object destroying itself, this method is static
// and designed to deal with one particular window index in the g_gui array.
{
if (aWindowIndex >= MAX_GUI_WINDOWS)
return FAIL;
if (!g_gui[aWindowIndex]) // It's already in the right state.
return OK;
GuiType &gui = *g_gui[aWindowIndex]; // For performance and convenience.
GuiIndexType u, gui_count;
if (gui.mHwnd)
{
// First destroy any windows owned by this window, since they will be auto-destroyed
// anyway due to their being owned. By destroying them explicitly, the Destroy()
// function is called recursively which keeps everything relatively neat.
for (u = 0, gui_count = 0; u < MAX_GUI_WINDOWS; ++u)
{
if (g_gui[u])
{
if (g_gui[u]->mOwner == gui.mHwnd)
GuiType::Destroy(u);
if (sGuiCount == ++gui_count) // No need to keep searching.
break;
}
}
// Testing shows that this must be done prior to calling DestroyWindow() later below, presumably
// because the destruction immediately destroys the status bar, or prevents it from answering messages.
// This seems at odds with MSDN's comment: "During the processing of [WM_DESTROY], it can be assumed
// that all child windows still exist".
if (gui.mStatusBarHwnd) // IsWindow(gui.mStatusBarHwnd) isn't called because even if possible for it to have been destroyed, SendMessage below should return 0.
{
// This is done because the vast majority of people wouldn't want to have to worry about it.
// They can always use DllCall() if they want to share the same HICON among multiple parts of
// the same bar, or among different windows (fairly rare).
HICON hicon;
LRESULT part_count = SendMessage(gui.mStatusBarHwnd, SB_GETPARTS, 0, NULL); // MSDN: "This message always returns the number of parts in the status bar [regardless of how it is called]".
for (LRESULT i = 0; i < part_count; ++i)
if (hicon = (HICON)SendMessage(gui.mStatusBarHwnd, SB_GETICON, i, 0))
DestroyIcon(hicon);
}
if (IsWindow(gui.mHwnd)) // If WM_DESTROY called us, the window might already be partially destroyed.
{
// If this window is using a menu bar but that menu is also used by some other window, first
// detatch the menu so that it doesn't get auto-destroyed with the window. This is done
// unconditionally since such a menu will be automatically destroyed when the script exits
// or when the menu is destroyed explicitly via the Menu command. It also prevents any
// submenus attached to the menu bar from being destroyed, since those submenus might be
// also used by other menus (however, this is not really an issue since menus destroyed
// would be automatically re-created upon next use). But in the case of a window that
// is currently using a menu bar, destroying that bar in conjunction with the destruction
// of some other window might cause bad side effects on some/all OSes.
ShowWindow(gui.mHwnd, SW_HIDE); // Hide it to prevent re-drawing due to menu removal.
SetMenu(gui.mHwnd, NULL);
if (!gui.mDestroyWindowHasBeenCalled)
{
gui.mDestroyWindowHasBeenCalled = true; // Signal the WM_DESTROY routine not to call us.
DestroyWindow(gui.mHwnd); // The WindowProc is immediately called and it now destroys the window.
}
// else WM_DESTROY was called by a function other than this one (possibly auto-destruct due to
// being owned by script's main window), so it would be bad to call DestroyWindow() again since
// it's already in progress.
}
} // if (gui.mHwnd)
if (gui.mBackgroundBrushWin)
DeleteObject(gui.mBackgroundBrushWin);
if (gui.mBackgroundBrushCtl)
DeleteObject(gui.mBackgroundBrushCtl);
if (gui.mHdrop)
DragFinish(gui.mHdrop);
// It seems best to delete the bitmaps whenever the control changes to a new image or
// whenever the control is destroyed. Otherwise, if a control or its parent window is
// destroyed and recreated many times, memory allocation would continue to grow from
// all the abandoned pointers:
for (u = 0; u < gui.mControlCount; ++u)
{
GuiControlType &control = gui.mControl[u];
if (control.type == GUI_CONTROL_PIC && control.union_hbitmap)
{
if (control.attrib & GUI_CONTROL_ATTRIB_ALTBEHAVIOR)
DestroyIcon((HICON)control.union_hbitmap); // Works on cursors too. See notes in LoadPicture().
else // union_hbitmap is a bitmap rather than an icon or cursor.
DeleteObject(control.union_hbitmap);
//else do nothing, since it isn't the right type to have a valid union_hbitmap member.
}
else if (control.type == GUI_CONTROL_LISTVIEW) // It was ensured at an earlier stage that union_lv_attrib != NULL.
free(control.union_lv_attrib);
}
// Not necessary since the object itself is about to be destroyed:
//gui.mHwnd = NULL;
//gui.mControlCount = 0; // All child windows (controls) are automatically destroyed with parent.
free(gui.mControl); // Free the control array, which was previously malloc'd.
delete g_gui[aWindowIndex]; // After this, the var "gui" is invalid so should not be referenced, i.e. the next line.
g_gui[aWindowIndex] = NULL;
--sGuiCount; // This count is maintained to help performance in the main event loop and other places.
if (icon_eligible_for_destruction && icon_eligible_for_destruction != g_script.mCustomIcon) // v1.0.37.07.
DestroyIconIfUnused(icon_eligible_for_destruction); // Must be done only after "g_gui[aWindowIndex] = NULL".
// For simplicity and performance, any fonts used *solely* by a destroyed window are destroyed
// only when the program terminates. Another reason for this is that sometimes a destroyed window
// is soon recreated to use the same fonts it did before.
return OK;
}
void GuiType::DestroyIconIfUnused(HICON ahIcon)
// Caller has ensured that the GUI window previously using ahIcon has been destroyed prior to calling
// this function.
{
if (!ahIcon) // Caller relies on this check.
return;
int i, gui_count;
for (i = 0, gui_count = 0; i < MAX_GUI_WINDOWS && gui_count < sGuiCount; ++i)
if (g_gui[i]) // This GUI window exists as an object.
{
// If another window is using this icon, don't destroy the because that has been reported to disrupt
// the window's display of the icon in some cases (apparently WM_SETICON doesn't make a copy of the
// icon). The windows still using the icon will be responsible for destroying it later.
if (g_gui[i]->mIconEligibleForDestruction == ahIcon)
return;
++gui_count;
}
// Since above didn't return, this icon is not currently in use by a GUI window. The caller has
// authorized us to destroy it.
DestroyIcon(ahIcon);
}
ResultType GuiType::Create()
{
if (mHwnd) // It already exists
return FAIL; // Seems best for now, since it shouldn't really be called this way.
// Use a separate class for GUI, which gives it a separate WindowProc and allows it to be more
// distinct when used with the ahk_class method of addressing windows.
static bool sGuiInitialized = false;
if (!sGuiInitialized)
{
WNDCLASSEX wc = {0};
wc.cbSize = sizeof(wc);
wc.lpszClassName = WINDOW_CLASS_GUI;
wc.hInstance = g_hInstance;
wc.lpfnWndProc = GuiWindowProc;
wc.hIcon = wc.hIconSm = (HICON)LoadImage(g_hInstance, MAKEINTRESOURCE(IDI_MAIN), IMAGE_ICON, 0, 0, LR_SHARED); // Use LR_SHARED to conserve memory (since the main icon is loaded for so many purposes).
wc.style = CS_DBLCLKS; // v1.0.44.12: CS_DBLCLKS is accepted as a good default by nearly everyone. It causes the window to receive WM_LBUTTONDBLCLK, WM_RBUTTONDBLCLK, and WM_MBUTTONDBLCLK (even without this, all windows receive WM_NCLBUTTONDBLCLK, WM_NCMBUTTONDBLCLK, and WM_NCRBUTTONDBLCLK).
// CS_HREDRAW and CS_VREDRAW are not included above because they cause extra flickering. It's generally better for a window to manage its own redrawing when it's resized.
main_icon = (HICON)LoadImage(g_hInstance, MAKEINTRESOURCE(IDI_MAIN), IMAGE_ICON, 0, 0, LR_SHARED); // Use LR_SHARED to conserve memory (since the main icon is loaded for so many purposes).
// Unlike mCustomIcon, leave mIconEligibleForDestruction NULL because a shared HICON such as one
// loaded via LR_SHARED should never be destroyed.
// Setting the small icon puts it in the upper left corner of the dialog window.
// Setting the big icon makes the dialog show up correctly in the Alt-Tab menu (but big seems to
// have no effect unless the window is unowned, i.e. it has a button on the task bar).
// Change for v1.0.37.07: Set both icons unconditionally for code simplicity, and also in case
// it's possible for the window to change after creation in a way that would make a custom icon
// become relevant. Set the big icon even if it's owned because there might be ways
// an owned window can have an entry in the alt-tab menu. The following ways come close
// but don't actually succeed:
// 1) It's owned by the main window but the main window isn't visible: It acquires the main window's icon
// in the alt-tab menu regardless of whether it was given a big icon of its own.
// 2) It's owned by another GUI window but it has the WS_EX_APPWINDOW style (might force a taskbar button):
// Same effect as in #1.
// 3) Possibly other ways.
SendMessage(mHwnd, WM_SETICON, ICON_SMALL, (LPARAM)main_icon); // Testing shows that a zero is returned for both;
SendMessage(mHwnd, WM_SETICON, ICON_BIG, (LPARAM)main_icon); // i.e. there is no previous icon to destroy in this case.
return OK;
}
void GuiType::SetLabels(char *aLabelPrefix)
// v1.0.44.09: Allow custom label prefix to be set; e.g. MyGUI vs. "5Gui" or "2Gui". This increases flexibility
// for scripts that dynamically create a varying number of windows, and also allows multiple windows to call the
// same set of subroutines.
// This function mustn't assume that mHwnd is a valid window because it might not have been created yet.
// Caller passes NULL to indicate "use default label prefix" (i.e. the WindowNumber followed by the string "Gui").
// Caller is reponsible for checking mLabelsHaveBeenSet as a pre-condition to calling us, if desired.
// Caller must ensure that mExStyle is up-to-date if mHwnd is an existing window. In addition, caller must
// apply any changes to mExStyle that we make here.
{
mLabelsHaveBeenSet = true; // Although it's value only matters in some contexts, it's set unconditionally for simplicity.
#define MAX_GUI_PREFIX_LENGTH 255
char *label_suffix, label_name[MAX_GUI_PREFIX_LENGTH+64]; // Labels are unlimited in length, but keep prefix+suffix relatively short so that it stays reasonable (to make it easier to limit it in the future should that ever be desirable).
if (aLabelPrefix)
strlcpy(label_name, aLabelPrefix, MAX_GUI_PREFIX_LENGTH+1); // Reserve the rest of label_name's size for the suffix below to ensure no chance of overflow.
else // Caller is indicating that the defaults should be used.
{
if (mWindowIndex > 0) // Prepend the window number for windows other than the first.
sprintf(label_name, "%dGui", mWindowIndex + 1);
else
strcpy(label_name, "Gui");
}
label_suffix = label_name + strlen(label_name); // This is the position at which the rest of the label name will be copied.
// Find the label to run automatically when the form closes (if any):
strcpy(label_suffix, "Close");
mLabelForClose = g_script.FindLabel(label_name); // OK if NULL (closing the window is the same as "gui, cancel").
// Find the label to run automatically when the user presses Escape (if any):
strcpy(label_suffix, "Escape");
mLabelForEscape = g_script.FindLabel(label_name); // OK if NULL (pressing ESCAPE does nothing).
// Find the label to run automatically when the user resizes the window (if any):
strcpy(label_suffix, "Size");
mLabelForSize = g_script.FindLabel(label_name); // OK if NULL.
// Find the label to run automatically when the user invokes context menu via AppsKey, Rightclick, or Shift-F10:
strcpy(label_suffix, "ContextMenu");
mLabelForContextMenu = g_script.FindLabel(label_name); // OK if NULL (leaves context menu unhandled).
// Find the label to run automatically when files are dropped onto the window:
strcpy(label_suffix, "DropFiles");
if ((mLabelForDropFiles = g_script.FindLabel(label_name)) // OK if NULL (dropping files is disallowed).
&& !mHdrop) // i.e. don't allow user to visibly drop files onto window if a drop is already queued or running.
mExStyle |= WS_EX_ACCEPTFILES; // Makes the window accept drops. Otherwise, the WM_DROPFILES msg is not received.
else
mExStyle &= ~WS_EX_ACCEPTFILES;
// It is not necessary to apply any style change made above because the caller detects changes and applies them.
}
void GuiType::UpdateMenuBars(HMENU aMenu)
// Caller has changed aMenu and wants the change visibly reflected in any windows that that
// use aMenu as a menu bar. For example, if a menu item has been disabled, the grey-color
// won't show up immediately unless the window is refreshed.
{
int i, gui_count;
for (i = 0, gui_count = 0; i < MAX_GUI_WINDOWS; ++i)
{
if (g_gui[i])
{
if (g_gui[i]->mHwnd && GetMenu(g_gui[i]->mHwnd) == aMenu && IsWindowVisible(g_gui[i]->mHwnd))
{
// Neither of the below two calls by itself is enough for all types of changes.
// Thought it's possible that every type of change only needs one or the other, both
// are done for simplicity:
// This first line is necessary at least for cases where the height of the menu bar
// (the number of rows needed to display all its items) has changed as a result
// of the caller's change. In addition, I believe SetWindowPos() must be called
// before RedrawWindow() to prevent artifacts in some cases:
if (aControlType == GUI_CONTROL_TAB2) // v1.0.47.05: Replace TAB2 with TAB at an early stage to simplify the code. The only purpose of TAB2 is to flag this as the new type of tab that avoids redrawing issues but has a new z-order that would break some existing scripts.
{
aControlType = GUI_CONTROL_TAB;
control.attrib |= GUI_CONTROL_ATTRIB_ALTBEHAVIOR; // v1.0.47.05: A means for new scripts to solve redrawing problems in tab controls at the cost of putting the tab control after its controls in the z-order.
}
if (aControlType == GUI_CONTROL_TAB)
{
if (mTabControlCount == MAX_TAB_CONTROLS)
return g_script.ScriptError("Too many tab controls." ERR_ABORT); // Short msg since so rare.
// For now, don't allow a tab control to be create inside another tab control because it raises
// doubt and probably would create complications. If it ever is allowed, note that
// control.tab_index stores this tab control's index (0 for the first tab control, 1 for the
// second, etc.) -- this is done for performance reasons.
control.tab_control_index = MAX_TAB_CONTROLS;
control.tab_index = mTabControlCount; // Store its control-index to help look-up performance in other sections.
}
else if (aControlType == GUI_CONTROL_STATUSBAR)
{
if (mStatusBarHwnd)
return g_script.ScriptError("Too many status bars." ERR_ABORT); // Short msg since so rare.
control.tab_control_index = MAX_TAB_CONTROLS; // Indicate that bar isn't owned by any tab control.
// No need to do the following because ZeroMem did it:
//control.tab_index = 0; // Ignored but set for maintainability/consistency.
// If this is the first control, set the default margin for the window based on the size
// of the current font, but only if the margins haven't already been set:
if (!mControlCount)
{
if (mMarginX == COORD_UNSPECIFIED)
mMarginX = (int)(1.25 * sFont[mCurrentFontIndex].point_size); // Seems to be a good rule of thumb.
if (mMarginY == COORD_UNSPECIFIED)
mMarginY = (int)(0.75 * sFont[mCurrentFontIndex].point_size); // Also seems good.
mPrevX = mMarginX; // This makes first control be positioned correctly if it lacks both X & Y coords.
}
control.type = aControlType; // Improves maintainability to do this early, but must be done after TAB2 vs. TAB is resolved higher above.
GuiControlOptionsType opt;
ControlInitOptions(opt, control);
// aOpt.checked is already okay since BST_UNCHECKED == 0
// Similarly, the zero-init of "control" higher above set the right values for password_char, new_section, etc.
/////////////////////////////////////////////////
// Set control-specific defaults for any options.
/////////////////////////////////////////////////
opt.style_add |= WS_VISIBLE; // Starting default for all control types.
opt.use_theme = mUseTheme; // Set default.
// Radio buttons are handled separately here, outside the switch() further below:
if (aControlType == GUI_CONTROL_RADIO)
{
// The BS_NOTIFY style is probably better not applied by default to radios because although it
// causes the control to send BN_DBLCLK messages, each double-click by the user is seen only
// as one click for the purpose of cosmetically making the button appear like it is being
// clicked rapidly. Update: the usefulness of double-clicking a radio button seems to
// outweigh the rare cosmetic deficiency of rapidly clicking a radio button, so it seems
// better to provide it as a default that can be overridden via explicit option.
// v1.0.47.04: Removed BS_MULTILINE from default because it is conditionally applied later below.
opt.style_add |= BS_NOTIFY; // No WS_TABSTOP here since that is applied elsewhere depending on radio group nature.
if (!mInRadioGroup)
opt.style_add |= WS_GROUP; // Tabstop must be handled later below.
// The mInRadioGroup flag will be changed accordingly after the control is successfully created.
//else by default, no WS_TABSTOP or WS_GROUP. However, WS_GROUP can be applied manually via the
// options list to split this radio group off from one immediately prior to it.
}
else // Not a radio.
if (mInRadioGroup) // Close out the prior radio group by giving this control the WS_GROUP style.
opt.style_add |= WS_GROUP; // This might not be necessary on all OSes, but it seems traditional / best-practice.
// Set control's default text color:
bool uses_font_and_text_color = USES_FONT_AND_TEXT_COLOR(aControlType); // Resolve macro only once.
if (uses_font_and_text_color) // Must check this to avoid corrupting union_hbitmap for PIC controls.
{
if (control.type != GUI_CONTROL_LISTVIEW) // Must check this to avoid corrupting union_lv_attrib.
control.union_color = mCurrentColor; // Default to the most recently set color.
opt.color_listview = mCurrentColor; // v1.0.44: Added so that ListViews start off with current font color unless overridden in their options.
}
else if (aControlType == GUI_CONTROL_PROGRESS) // This must be done to detect custom Progress color.
control.union_color = CLR_DEFAULT; // Set progress to default color avoids unnecessary stripping of theme.
//else don't change union_color since it shares the same address as union_hbitmap & union_col.
switch (aControlType) // Set starting defaults based on control type (the above also does some of that).
{
// Some controls also have the WS_EX_CLIENTEDGE exstyle by default because they look pretty strange
// without them. This seems to be the standard default used by most applications.
// Note: It seems that WS_BORDER is hardly ever used in practice with controls, just parent windows.
case GUI_CONTROL_DROPDOWNLIST:
opt.style_add |= WS_TABSTOP|WS_VSCROLL; // CBS_DROPDOWNLIST is forcibly applied later. WS_VSCROLL is necessary.
break;
case GUI_CONTROL_COMBOBOX:
// CBS_DROPDOWN is set as the default here to allow the flexibilty for it to be changed to
// CBS_SIMPLE. CBS_SIMPLE is allowed for ComboBox but not DropDownList because CBS_SIMPLE
// has an edit control just like a combo, which DropDownList isn't equipped to handle via Submit().
// Also, if CBS_AUTOHSCROLL is omitted, typed text cannot go beyond the visible width of the
// edit control, so it seems best to havethat as a default also:
opt.style_add |= WS_TABSTOP|WS_VSCROLL|CBS_AUTOHSCROLL|CBS_DROPDOWN; // WS_VSCROLL is necessary.
break;
case GUI_CONTROL_LISTBOX:
// Omit LBS_STANDARD because it includes LBS_SORT, which we don't want as a default style.
// However, as of v1.0.30.03, LBS_USETABSTOPS is included by default because:
// 1) Not doing so seems to make it impossible to apply tab stops after the control has been created.
// 2) Without this style, tabs appears as empty squares in the text, which seems undesirable for
// 99.9% of applications.
// 3) LBS_USETABSTOPS can be explicitly removed by specifying -0x80 in the options of "Gui Add".
opt.style_add |= WS_TABSTOP|WS_VSCROLL|LBS_USETABSTOPS; // WS_VSCROLL seems the most desirable default.
opt.exstyle_add |= WS_EX_CLIENTEDGE;
break;
case GUI_CONTROL_LISTVIEW:
// The ListView extended styles are actually an entirely separate class of styles that exist
// separately from ExStyles. This explains why Get/SetWindowLong doesn't work on them.
// But keep in mind that some of the normal/classic extended styles can still be applied
// to a ListView via Get/SetWindowLong.
// The listview extended styles all require at least ComCtl32.dll 4.70 (some might require more)
// and thus will have no effect in Win 95/NT unless they have MSIE 3.x or similar patch installed.
// Thus, things like LVS_EX_FULLROWSELECT and LVS_EX_HEADERDRAGDROP will have no effect on those systems.
opt.listview_style |= LVS_EX_FULLROWSELECT|LVS_EX_HEADERDRAGDROP; // LVS_AUTOARRANGE seems to disrupt the display of the column separators and have other weird effects in Report view.
opt.style_add |= WS_TABSTOP|LVS_SHOWSELALWAYS; // LVS_REPORT is omitted to help catch bugs involving opt.listview_view. WS_THICKFRAME allows the control itself to be drag-resized.
opt.exstyle_add |= WS_EX_CLIENTEDGE; // WS_EX_STATICEDGE/WS_EX_WINDOWEDGE/WS_BORDER(non-ex) don't look as nice. WS_EX_DLGMODALFRAME is a weird but interesting effect.
opt.listview_view = LVS_REPORT; // Improves maintainability by avoiding the need to check if it's -1 in other places.
break;
case GUI_CONTROL_TREEVIEW:
// Default style is somewhat debatable, but the familiarity of Explorer's own defaults seems best.
// TVS_SHOWSELALWAYS seems preferable by most people, and is also consistent to what is used for ListView.
// Lines and buttons also seem preferable because the main feature of a tree is its hierarchical nature,
// and that nature isn't well revealed without buttons, and buttons can't be shown at the root level
// without TVS_LINESATROOT, which in turn can't be active without TVS_HASLINES.
opt.style_add |= WS_TABSTOP|TVS_SHOWSELALWAYS|TVS_HASLINES|TVS_LINESATROOT|TVS_HASBUTTONS; // TVS_LINESATROOT is necessary to get plus/minus buttons on root-level items.
opt.exstyle_add |= WS_EX_CLIENTEDGE; // Debatable, but seems best for consistency with ListView.
break;
case GUI_CONTROL_EDIT:
opt.style_add |= WS_TABSTOP;
opt.exstyle_add |= WS_EX_CLIENTEDGE;
break;
case GUI_CONTROL_UPDOWN:
// UDS_NOTHOUSANDS is debatable:
// 1) The primary means by which a script validates whether the buddy contains an invalid
// or out-of-range value for its UpDown is to compare the contents of the two. If one
// has commas and the other doesn't, the commas must first be removed before comparing.
// 2) Presence of commas in numeric data is going to be a source of script bugs for those
// who take the buddy's contents rather than the UpDown's contents as the user input.
// However, you could argue that script is not proper if it does this blindly because
// the buddy could contain an out-of-range or non-numeric value.
// 3) Display is more ergonomic if it has commas in it.
// The above make it pretty hard to decide, so sticking with the default of have commas
// seems ok. Also, UDS_ALIGNRIGHT must be present by default because otherwise buddying
if (opt.color_bk == CLR_DEFAULT) // i.e. the options list must have explicitly specified BackgroundDefault.
opt.color_bk = CLR_INVALID; // Tell things like ControlSetListViewOptions "no color change needed".
else if (opt.color_bk == CLR_INVALID && mBackgroundColorCtl != CLR_DEFAULT // No bk color was specified in options param.
&& aControlType != GUI_CONTROL_PROGRESS && aControlType != GUI_CONTROL_STATUSBAR) // And the control obeys the current "Gui, Color,, CtlBkColor". Status bars don't obey it because it seems slightly less desirable for most people, and also because system default bar color might be diff. than system default win color on some themes.
// Since bkgnd color was not explicitly specified in options, use the current background color (except progress bars, which do their own thing).
opt.color_bk = mBackgroundColorCtl; // Use window's global custom, control background.
//else leave it as invalid so that ControlSetListView/TreeView/ProgressOptions() etc. won't bother changing it.
// Change for v1.0.45 (buttons) and v1.0.47.04 (checkboxes and radios): Under some desktop themes and
// unusual DPI settings, it has been reported that the last letter of the control's text gets truncated
// and/or causes an unwanted wrap that prevents proper display of the text. To solve this, default to
// "wrapping enabled" only when necessary. One case it's usually necessary is when there's an explicit
// width present because then the text can automatically word-wrap to the next line if it contains any
// spaces/tabs/dashes (this also improves backward compatibility).
|| opt.row_count > 1.5 || StrChrAny(aText, "\n\r")) // Both LF and CR can start new lines.
? (BS_MULTILINE & ~opt.style_remove) // Add BS_MULTILINE unless it was explicitly removed.
: 0; // Otherwise: Omit BS_MULTILINE (unless it was explicitly added [the "0" is verified correct]) because on some unsuual DPI settings (i.e. DPIs other than 96 or 120), DrawText() sometimes yields a width that is slightly too narrow, which causes unwanted wrapping in single-line checkboxes/radios/buttons.
style |= WS_CHILD; // All control types must have this, even if script attempted to remove it explicitly.
switch (aControlType)
{
case GUI_CONTROL_GROUPBOX:
// There doesn't seem to be any flexibility lost by forcing the buttons to be the right type,
// and doing so improves maintainability and peace-of-mind:
style = (style & ~BS_TYPEMASK) | BS_GROUPBOX; // Force it to be the right type of button.
break;
case GUI_CONTROL_BUTTON:
if (style & BS_DEFPUSHBUTTON) // i.e. its single bit is present. BS_TYPEMASK is not involved in this line because it's a purity check.
style = (style & ~BS_TYPEMASK) | BS_DEFPUSHBUTTON; // Done to ensure the lowest four bits are pure.
else
style &= ~BS_TYPEMASK; // Force it to be the right type of button --> BS_PUSHBUTTON == 0
style |= contains_bs_multiline_if_applicable;
break;
case GUI_CONTROL_CHECKBOX:
// Note: BS_AUTO3STATE and BS_AUTOCHECKBOX are mutually exclusive due to their overlap within
// the bit field:
if ((style & BS_AUTO3STATE) == BS_AUTO3STATE) // Fixed for v1.0.45.03 to check if all the BS_AUTO3STATE bits are present, not just "any" of them. BS_TYPEMASK is not involved here because this is a purity check, and TYPEMASK would defeat the whole purpose.
style = (style & ~BS_TYPEMASK) | BS_AUTO3STATE; // Done to ensure the lowest four bits are pure.
else
style = (style & ~BS_TYPEMASK) | BS_AUTOCHECKBOX; // Force it to be the right type of button.
style |= contains_bs_multiline_if_applicable; // v1.0.47.04: Added to avoid unwanted wrapping on systems with unusual DPI settings (DPIs other than 96 and 120 sometimes seem to cause a roundoff problem with DrawText()).
break;
case GUI_CONTROL_RADIO:
style = (style & ~BS_TYPEMASK) | BS_AUTORADIOBUTTON; // Force it to be the right type of button.
// This below must be handled here rather than in the set-defaults section because this
// radio might be the first of its group due to the script having explicitly specified the word
// Group in options (useful to make two adjacent radio groups).
if (style & WS_GROUP && !(opt.style_remove & WS_TABSTOP))
style |= WS_TABSTOP;
// Otherwise it lacks a tabstop by default.
style |= contains_bs_multiline_if_applicable; // v1.0.47.04: Added to avoid unwanted wrapping on systems with unusual DPI settings (DPIs other than 96 and 120 sometimes seem to cause a roundoff problem with DrawText()).
break;
case GUI_CONTROL_DROPDOWNLIST:
style |= CBS_DROPDOWNLIST; // This works because CBS_DROPDOWNLIST == CBS_SIMPLE|CBS_DROPDOWN
break;
case GUI_CONTROL_COMBOBOX:
if (style & CBS_SIMPLE) // i.e. CBS_SIMPLE has been added to the original default, so assume it is SIMPLE.
style = (style & ~0x0F) | CBS_SIMPLE; // Done to ensure the lowest four bits are pure.
else
style = (style & ~0x0F) | CBS_DROPDOWN; // Done to ensure the lowest four bits are pure.
break;
case GUI_CONTROL_LISTBOX:
style |= LBS_NOTIFY; // There doesn't seem to be any flexibility lost by forcing this style.
break;
case GUI_CONTROL_EDIT:
// This is done for maintainability and peace-of-mind, though it might not strictly be required
// to be done at this stage:
if (opt.row_count > 1.5 || strchr(aText, '\n')) // Multiple rows or contents contain newline.
style |= (ES_MULTILINE & ~opt.style_remove); // Add multiline unless it was explicitly removed.
// This next check is relied upon by other things. If this edit has the multiline style either
// due to the above check or any other reason, provide other default styles if those styles
// weren't explicitly removed in the options list:
if (style & ES_MULTILINE) // If allowed, enable vertical scrollbar and capturing of ENTER keystrokes.
// Safest to include ES_AUTOVSCROLL, though it appears to have no effect on XP. See also notes below:
// Remove spaces and ampersands. Although ampersands are legal in labels, it seems
// more friendly to omit them in the automatic-label label name. Note that a button
// or menu item can contain a literal ampersand by using two ampersands, such as
// "Save && Exit" (in this example, the auto-label would be named "ButtonSaveExit").
// v1.0.46.01: tabs and accents are also removed since labels can't contain them.
// However, colons are NOT removed because labels CAN contain them (except at the very end;
// but due to rarity and backward compatibility, it doesn't seem worth adding code size for that).
StrReplace(label_name, "\r", "", SCS_SENSITIVE);
StrReplace(label_name, "\n", "", SCS_SENSITIVE);
StrReplace(label_name, "\t", "", SCS_SENSITIVE);
StrReplace(label_name, " ", "", SCS_SENSITIVE);
StrReplace(label_name, "&", "", SCS_SENSITIVE);
StrReplace(label_name, "`", "", SCS_SENSITIVE);
// Alternate method, but seems considerably larger in code size based on OBJ size:
//char *string_list[] = {"\r", "\n", " ", "\t", "&", "`", NULL}; // \r is separate from \n in case they're ever unpaired. Last char must be NULL to terminate the list.
// Assign a default just to allow the control to be created successfully. 13 is the default
// height of a text/radio control for the typical 8 point font size, but the exact value
// shouldn't matter (within reason) since calc_control_height_from_row_count is telling us this type of
// control will not obey the height anyway. Update: It seems better to use a small constant
// value to help catch bugs while still allowing the control to be created:
if (!calc_height_later)
opt.height = 30;
//else MONTHCAL and others must keep their "unspecified height" value for later detection.
}
bool control_width_was_set_by_contents = false;
if (opt.height == COORD_UNSPECIFIED || opt.width == COORD_UNSPECIFIED)
{
// Set defaults:
int extra_width = 0;
UINT draw_format = DT_CALCRECT;
switch (aControlType)
{
case GUI_CONTROL_EDIT:
if (!*aText) // Only auto-calculate edit's dimensions if there is text to do it with.
break;
// Since edit controls leave approximate 1 avg-char-width margin on the right side,
// and probably exactly 4 pixels on the left counting its border and the internal
// margin), adjust accordingly so that DrawText() will calculate the correct
// control height based on word-wrapping. Note: Can't use EM_GETRECT because
// control doesn't exist yet (though that might be an alternative approach for
// the future):
GUI_SET_HDC
GetTextMetrics(hdc, &tm);
extra_width += 4 + tm.tmAveCharWidth;
// Determine whether there will be a vertical scrollbar present. If ES_MULTILINE hasn't
// already been applied or auto-detected above, it's possible that a scrollbar will be
// added later due to the text auto-wrapping. In that case, the calculated height may
// be incorrect due to the additional wrapping caused by the width taken up by the
// scrollbar. Since this combination of circumstances is rare, and since there are easy
// workarounds, it's just documented here as a limitation:
if (style & WS_VSCROLL)
extra_width += GetSystemMetrics(SM_CXVSCROLL);
// DT_EDITCONTROL: "the average character width is calculated in the same manner as for an edit control"
// It might help some aspects of the estimate conducted below.
// Also include DT_EXPANDTABS under the assumption that if there are tabs present, the user
// intended for them to be there because a multiline edit would expand them (rather than trying
// to worry about whether this control *might* become auto-multiline after this point.
draw_format |= DT_EXPANDTABS|DT_EDITCONTROL|DT_NOPREFIX; // v1.0.44.10: Added DT_NOPREFIX because otherwise, if the text contains & or &&, the control won't be sized properly.
// and now fall through and have the dimensions calculated based on what's in the control.
// ABOVE FALLS THROUGH TO BELOW
case GUI_CONTROL_TEXT:
case GUI_CONTROL_BUTTON:
case GUI_CONTROL_CHECKBOX:
case GUI_CONTROL_RADIO:
{
GUI_SET_HDC
if (aControlType == GUI_CONTROL_TEXT)
{
draw_format |= DT_EXPANDTABS; // Buttons can't expand tabs, so don't add this for them.
if (style & SS_NOPREFIX) // v1.0.44.10: This is necessary to auto-width the control properly if its contents include any ampersands.
draw_format |= DT_NOPREFIX;
}
else if (aControlType == GUI_CONTROL_CHECKBOX || aControlType == GUI_CONTROL_RADIO)
{
// Both Checkbox and Radio seem to have the same spacing characteristics:
// Expand to allow room for button itself, its border, and the space between
// the button and the first character of its label (this space seems to
// be the same as tmAveCharWidth). +2 seems to be needed to make it work
// for the various sizes of Courier New vs. Verdana that I tested. The
// alternative, (2 * GetSystemMetrics(SM_CXEDGE)), seems to add a little
// too much width (namely 4 vs. 2).
GetTextMetrics(hdc, &tm);
extra_width += GetSystemMetrics(SM_CXMENUCHECK) + tm.tmAveCharWidth + 2; // v1.0.40.03: Reverted to +2 vs. +3 (it had been changed to +3 in v1.0.40.01).
}
if (opt.width != COORD_UNSPECIFIED) // Since a width was given, auto-expand the height via word-wrapping.
int gui_standard_width = GUI_STANDARD_WIDTH; // Resolve macro only once for performance/code size.
switch(aControlType)
{
case GUI_CONTROL_DROPDOWNLIST:
case GUI_CONTROL_COMBOBOX:
case GUI_CONTROL_LISTBOX:
case GUI_CONTROL_HOTKEY:
case GUI_CONTROL_EDIT:
opt.width = gui_standard_width;
break;
case GUI_CONTROL_LISTVIEW:
case GUI_CONTROL_TREEVIEW:
case GUI_CONTROL_DATETIME: // Seems better to have wider default to fit LongDate and because drop-down calendar is fairly wide (though the latter is a weak reason).
opt.width = gui_standard_width * 2;
break;
case GUI_CONTROL_UPDOWN: // Iffy, but needs some kind of default?
opt.width = (style & UDS_HORZ) ? gui_standard_width : PROGRESS_DEFAULT_THICKNESS; // Progress's default seems ok for up-down too.
break;
case GUI_CONTROL_SLIDER:
// Make vertical trackbars narrow by default. For vertical trackbars: there doesn't seem
// to be much point in defaulting the width to something proportional to font size because
// the thumb only seems to have two sizes and doesn't auto-grow any larger than that.
// Strangely, in spite of the control having been created with the BS_DEFPUSHBUTTON style,
// need to send BM_SETSTYLE or else the default button will lack its visual style when the
// dialog is first shown. Also strange is that the following must be done *after*
// removing the visual/default style from the old default button and/or after doing
// DM_SETDEFID above.
SendMessage(control.hwnd, BM_SETSTYLE, (WPARAM)LOWORD(style), MAKELPARAM(TRUE, 0)); // Redraw = yes. It's probably smart enough not to do it if the window is hidden.
}
}
break;
case GUI_CONTROL_CHECKBOX:
// The BS_NOTIFY style is not a good idea for checkboxes because although it causes the control
// to send BN_DBLCLK messages, any rapid clicks by the user on (for example) a tri-state checkbox
// are seen only as one click for the purpose of changing the box's state.
if (control.hwnd = CreateWindowEx(exstyle, "button", aText, style
// Assume bar will be actually appear even though it won't in the rare case where
// its specified pixel-width is smaller than the width of the window:
opt.height += GetSystemMetrics(SM_CYHSCROLL);
}
MoveWindow(control.hwnd, opt.x, opt.y, opt.width, opt.height, TRUE); // Repaint, since it might be visible.
// Since by default, the OS adjusts list's height to prevent a partial item from showing
// (LBS_NOINTEGRALHEIGHT), fetch the actual height for possible use in positioning the
// next control:
retrieve_dimensions = true;
}
break;
case GUI_CONTROL_LISTVIEW:
if (opt.listview_view != LV_VIEW_TILE) // It was ensured earlier that listview_view can be set to LV_VIEW_TILE only for XP or later.
style = (style & ~LVS_TYPEMASK) | opt.listview_view; // Create control in the correct view mode whenever possible (TILE is the exception because it can't be expressed via style).
if (control.hwnd = CreateWindowEx(exstyle, WC_LISTVIEW, "", style, opt.x, opt.y // exstyle does apply to ListViews.
if (opt.listview_style) // This is a third set of styles that exist in addition to normal & extended.
ListView_SetExtendedListViewStyle(control.hwnd, opt.listview_style); // No return value. Will have no effect on Win95/NT that lack comctl32.dll 4.70+ distributed with MSIE 3.x.
ControlSetListViewOptions(control, opt); // Relies on adjustments to opt.color_changed and color_bk done higher above.
if (opt.height == COORD_UNSPECIFIED) // Adjust the control's size to fit opt.row_count rows.
{
// Known limitation: This will be inaccurate if an ImageList is later assigned to the
// ListView because that increases the height of each row slightly (or a lot if
// a large-icon list is forced into Details/Report view and other views that are
// traditionally small-icon). The code size and complexity of trying to compensate
// for this doesn't seem likely to be worth it.
GUI_SETFONT // Required before asking it for a height estimate.
switch (opt.listview_view)
{
case LVS_REPORT:
// The following formula has been tested on XP with the point sizes 8, 9, 10, 12, 14, and 18 for:
ControlSetTreeViewOptions(control, opt); // Relies on adjustments to opt.color_changed and color_bk done higher above.
if (opt.himagelist) // Currently only supported upon creation, not via GuiControl, since in that case the decision of whether to destroy the old imagelist would be uncertain.
TreeView_SetImageList(control.hwnd, opt.himagelist, TVSIL_NORMAL); // Currently no error reporting.
if (opt.height == COORD_UNSPECIFIED) // Adjust the control's size to fit opt.row_count rows.
{
// Known limitation (may exist for TreeViews the same as it does for ListViews):
// The follow might be inaccurate if an ImageList is later assigned to the TreeView because
// that may increase the height of each row. The code size and complexity of trying to
// compensate for this doesn't seem likely to be worth it.
GUI_SETFONT // Required before asking it for a height estimate.
//else keep the default or the format set via style higher above.
// Feels safter to do range prior to selection even though unlike GUI_CONTROL_MONTHCAL,
// GUI_CONTROL_DATETIME tolerates them in the reverse order when one doesn't fit the other.
if (opt.gdtr_range) // If the date/time set above is invalid in light of the following new range, the date will be automatically to the closest valid date.
// Note: The DateTime_SetMonthCalFont() macro is never used because apparently it's not required
// to set the font, or even to repaint.
}
break;
}
case GUI_CONTROL_MONTHCAL:
if (!opt.gdtr && *aText) // The option "ChooseYYYYMMDD" was not present, so fall back to Text (allow Text to be ignored in case it's incorrectly a date-time format, etc.)
if (style & MCS_MULTISELECT) // Must do this prior to setting initial contents in case contents is a range greater than 7 days.
MonthCal_SetMaxSelCount(control.hwnd, 366); // 7 days seems too restrictive a default, so expand.
if (opt.gdtr_range) // If the date/time set above is invalid in light of the following new range, the date will be automatically to the closest valid date.
opt.height = rect.bottom; // Init for default and for use below (room for only a single month's height).
if (opt.row_count > 0 && opt.row_count != 1.0) // row_count was explicitly specified by the script, so use its exact value, even if it isn't a whole number (for flexibility).
{
// Unlike horizontally stacked calendars, vertically stacking them produces no separator
// between them.
GUI_SET_HDC
GetTextMetrics(hdc, &tm);
// If there will be no today string, the height reported by MonthCal_GetMinReqRect
// is not correct for use in calculating the height of more than one month stacked
// vertically. Must adjust it to make it display properly.
if (style & MCS_NOTODAY) // No today string, but space is still reserved for it, so must compensate for that.
opt.height += tm.tmHeight + 4; // Formula tested with Courier New and Verdana 8/10/12 with row counts between 1 and 5.
opt.height = (int)(opt.height * opt.row_count); // Calculate height of all months.
// Regardless of whether MCS_NOTODAY is present, the below is still the right formula.
// Room for the today-string is reserved only once at the bottom (even without MCS_NOTODAY),
// so need to subtract that (also note that some months have 6 rows and others only 5,
// but there is whitespace padding in the case of 5 to make all months the same height).
// v1.0.42.02: The below is a fix for tab controls that contain a ListView so that up-downs in the
// tab control don't snap onto the tab control (due to the z-order change done by the ListView creation
// section whenever a ListView exists inside a tab control).
bool provide_buddy_manually;
if ( provide_buddy_manually = (style & UDS_AUTOBUDDY)
&& (mStatusBarHwnd // Added for v1.0.44.01 (fixed in v1.0.44.04): Since the status bar is pushed to the bottom of the z-order after adding each other control, must do manual buddying whenever an UpDown is added after the status bar (to prevent it from attaching to the status bar).
|| (owning_tab_control // mControlCount is greater than zero whenever owning_tab_control!=NULL
// v1.0.44: Must keep status bar at the bottom of the z-order so that it gets drawn last. This alleviates
// (but does not completely prevent) other controls from overlapping it and getting drawn on top. This is
// done each time a control is added -- rather than at some single time such as when the parent window is
// first shown -- in case the script adds more controls later.
if (mStatusBarHwnd) // Seems harmless to do it even if the just-added control IS the status bar. Also relies on the fact that that only one status bar is allowed.
, SWP_NOMOVE|SWP_NOSIZE|SWP_NOACTIVATE); // SWP_NOACTIVATE prevents the side-effect of activating the window, which is undesirable if only its style is changing.
// Fix for v1.0.41.01: Update the original style too, so that the call to SetWindowLong() later below
// is made only if multiple styles are being changed on the same line, e.g. Gui +Disabled -SysMenu
if (adding) exstyle_orig |= WS_EX_TOPMOST; else exstyle_orig &= ~WS_EX_TOPMOST;
}
// Fix for v1.0.41.01: The following line is now done unconditionally. Previously, it wasn't
// done if the window already existed, which caused an example such as the following to first
// set the window always on top and then immediately afterward try to unset it via SetWindowLong
// (because mExStyle hadn't been updated to reflect the change made by SetWindowPos):
// Gui, +AlwaysOnTop +Disabled -SysMenu
if (adding) mExStyle |= WS_EX_TOPMOST; else mExStyle &= ~WS_EX_TOPMOST;
}
else if (!stricmp(next_option, "Border"))
if (adding) mStyle |= WS_BORDER; else mStyle &= ~WS_BORDER;
else if (!stricmp(next_option, "Caption"))
// To remove title bar successfully, the WS_POPUP style must also be applied:
// For simplicity, the value of "adding" is ignored since no use is forseeable for "-Delimiter".
if (!stricmp(next_option, "Tab"))
mDelimiter = '\t';
else if (!stricmp(next_option, "Space"))
mDelimiter = ' ';
else
mDelimiter = *next_option ? *next_option : '|';
}
else if (!stricmp(next_option, "Disabled"))
{
if (mHwnd)
{
EnableWindow(mHwnd, adding ? FALSE : TRUE); // Must not not apply WS_DISABLED directly because that breaks the window.
// Fix for v1.0.41.01: Update the original style too, so that the call to SetWindowLong() later below
// is made only if multiple styles are being changed on the same line, e.g. Gui +Disabled -SysMenu
if (adding) style_orig |= WS_DISABLED; else style_orig &= ~WS_DISABLED;
}
// Fix for v1.0.41.01: The following line is now done unconditionally. Previously, it wasn't
// done if the window already existed, which caused an example such as the following to first
// disable the window and then immediately afterward try to enable it via SetWindowLong
// (because mStyle hadn't been updated to reflect the change made by SetWindowPos):
// Gui, +AlwaysOnTop +Disabled -SysMenu
if (adding) mStyle |= WS_DISABLED; else mStyle &= ~WS_DISABLED;
}
else if (!strnicmp(next_option, "Label", 5)) // v1.0.44.09: Allow custom label prefix for the reasons described in SetLabels().
{
if (adding)
SetLabels(next_option + 5);
//else !adding (-Label), which currently does nothing. Potential future uses include:
// Disable all labels (seems too rare to be useful).
// Revert to defaults (e.g. 2GuiSize): Doesn't seem to be of much value because the caller will likely
// always know the number of the window in question (if nothing else, than via A_Gui) and can thus revert
// to defaults via something like +Label%A_Gui%Gui.
// Alternative: Could also use some char that's illegal in labels to indicate one or more of the above.
}
else if (!strnicmp(next_option, "LastFound", 9)) // strnicmp so that "LastFoundExist" is also recognized.
aSetLastFoundWindow = true; // Regardless of whether "adding" is true or false.
else if (!stricmp(next_option, "MaximizeBox")) // See above comment.
if (adding) mStyle |= WS_MAXIMIZEBOX|WS_SYSMENU; else mStyle &= ~WS_MAXIMIZEBOX;
else if (!stricmp(next_option, "MinimizeBox"))
// WS_MINIMIZEBOX requires WS_SYSMENU to take effect. It can be explicitly omitted
// via "+MinimizeBox -SysMenu" if that functionality is ever needed.
if (adding) mStyle |= WS_MINIMIZEBOX|WS_SYSMENU; else mStyle &= ~WS_MINIMIZEBOX;
else if (!strnicmp(next_option, "MinSize", 7)) // v1.0.44.13: Added for use with WM_GETMINMAXINFO.
{
next_option += 7;
if (adding)
{
if (*next_option)
{
// The following will retrieve zeros if window hasn't yet been shown for the first time,
// in which case the first showing will do the NC adjustment for us. The overall approach
// used here was chose to avoid any chance for Min/MaxSize to be adjusted more than once
// to convert client size to entire-size, which would be wrong since the adjustment must be
// applied only once. Examples of such situations are when one of the coordinates is omitted,
// or when +MinSize is specified prior to the first "Gui Show" but +MaxSize is specified after.
GetNonClientArea(nc_width, nc_height);
// atoi() vs. ATOI() is used below to avoid ambiguity of "x" being hex 0x vs. a delimiter.
if ((pos_of_the_x = StrChrAny(next_option, "Xx")) && pos_of_the_x[1]) // Kept simple due to rarity of transgressions and their being inconsequential.
mMinHeight = atoi(pos_of_the_x + 1) + nc_height;
//else it's "MinSize333" or "MinSize333x", so leave height unchanged as documented.
if (pos_of_the_x != next_option) // There's no 'x' or it lies to the right of next_option.
mMinWidth = atoi(next_option) + nc_width; // atoi() automatically stops converting when it reaches non-numeric character.
//else it's "MinSizeX333", so leave width unchanged as documented.
}
else // Since no width or height was specified:
// Use the window's current size. But if window hasn't yet been shown for the
// first time, this will set the values to COORD_CENTERED, which tells the
// first-show routine to get the total width/height upon first showing (since
// that's where the window's initial size is determined).
GetTotalWidthAndHeight(mMinWidth, mMinHeight);
}
else // "-MinSize", so tell the WM_GETMINMAXINFO handler to use system defaults.
{
mMinWidth = COORD_UNSPECIFIED;
mMinHeight = COORD_UNSPECIFIED;
}
}
else if (!strnicmp(next_option, "MaxSize", 7)) // v1.0.44.13: Added for use with WM_GETMINMAXINFO.
{
// SEE "MinSize" section above for more comments because the section below is nearly identical to it.
next_option += 7;
if (adding)
{
if (*next_option)
{
GetNonClientArea(nc_width, nc_height);
if ((pos_of_the_x = StrChrAny(next_option, "Xx")) && pos_of_the_x[1]) // Kept simple due to rarity of transgressions and their being inconsequential.
mMaxHeight = atoi(pos_of_the_x + 1) + nc_height;
if (pos_of_the_x != next_option) // There's no 'x' or it lies to the right of next_option.
mMaxWidth = atoi(next_option) + nc_width; // atoi() automatically stops converting when it reaches non-numeric character.
}
else // No width or height was specified. See comment in "MinSize" for details about this.
GetTotalWidthAndHeight(mMaxWidth, mMaxHeight); // If window hasn't yet been shown for the first time, this will set them to COORD_CENTERED, which tells the first-show routine to get the total width/height.
}
else // "-MaxSize", so tell the WM_GETMINMAXINFO handler to use system defaults.
ShowWindow(mHwnd, SW_SHOWNA); // i.e. don't activate it if it wasn't before. Note that SW_SHOWNA avoids restoring the window if it is currently minimized or maximized (unlike SW_SHOWNOACTIVATE).
// None of the following methods alone is enough, at least not when the window is currently active:
// Continue on to create the window so that code is simplified in other places by
// using the assumption that "if gui[i] object exists, so does its window".
// Another important reason this is done is that if an owner window were to be destroyed
// before the window it owns is actually created, the WM_DESTROY logic would have to check
// for any windows owned by the window being destroyed and update them.
}
return OK;
}
void GuiType::GetNonClientArea(LONG &aWidth, LONG &aHeight)
// Added for v1.0.44.13.
// Yields only the *extra* width/height added by the windows non-client area.
// If the window hasn't been shown for the first time, the caller wants zeros.
// The reason for making the script specify size of client area rather than entire window is that it
// seems far more useful. For example, a script might know exactly how much minimum height its
// controls require in the client area, but would find it inconvenient to have to take into account
// the height of the title bar and menu bar (which vary depending on theme and other settings).
{
if (mGuiShowHasNeverBeenDone) // In this case, the script might not yet have added the menu bar and other styles that affect the size of the non-client area. So caller wants to do these calculations later.
{
aWidth = 0;
aHeight = 0;
return;
}
// Otherwise, mGuiShowHasNeverBeenDone==false, which should mean that mHwnd!=NULL.
RECT rect, client_rect;
GetWindowRect(mHwnd, &rect);
GetClientRect(mHwnd, &client_rect); // Client rect's left & top are always zero.
// For code simplicity, both min and max must be present to enable a selected-range.
if (aOpt.gdtr == (GDTR_MIN | GDTR_MAX))
aOpt.style_add |= MCS_MULTISELECT;
//else never remove the style since it's valid to create a range-capable control via
// "Multi" that has only a single date selected (or none). Also, if the control already
// exists, MSDN says that MCS_MULTISELECT cannot be added or removed.
break;
default:
aOpt.choice = ATOI(next_option);
if (aOpt.choice < 1) // Invalid: number should be 1 or greater.
aOpt.choice = 0; // Flag it as invalid.
}
}
//else do nothing (not currently implemented)
}
// Styles (general):
else if (!stricmp(next_option, "Border"))
if (adding) aOpt.style_add |= WS_BORDER; else aOpt.style_remove |= WS_BORDER;
else if (!stricmp(next_option, "VScroll")) // Seems harmless in this case not to check aControl.type to ensure it's an input-capable control.
if (adding) aOpt.style_add |= WS_VSCROLL; else aOpt.style_remove |= WS_VSCROLL;
else if (!strnicmp(next_option, "HScroll", 7)) // Seems harmless in this case not to check aControl.type to ensure it's an input-capable control.
{
if (aControl.type == GUI_CONTROL_TREEVIEW)
// Testing shows that Tree doesn't seem to fully support removal of hscroll bar after creation.
if (adding) aOpt.style_remove |= TVS_NOHSCROLL; else aOpt.style_add |= TVS_NOHSCROLL;
else
if (adding)
{
// MSDN: "To respond to the LB_SETHORIZONTALEXTENT message, the list box must have
// been defined with the WS_HSCROLL style."
aOpt.style_add |= WS_HSCROLL;
next_option += 7;
aOpt.hscroll_pixels = *next_option ? ATOI(next_option) : -1; // -1 signals it to use a default based on control's width.
}
else
aOpt.style_remove |= WS_HSCROLL;
}
else if (!stricmp(next_option, "Tabstop")) // Seems harmless in this case not to check aControl.type to ensure it's an input-capable control.
if (adding) aOpt.style_add |= WS_TABSTOP; else aOpt.style_remove |= WS_TABSTOP;
else if (!stricmp(next_option, "NoTab")) // Supported for backward compatibility and it might be more ergonomic for "Gui Add".
if (adding) aOpt.style_remove |= WS_TABSTOP; else aOpt.style_add |= WS_TABSTOP;
else if (!stricmp(next_option, "Group")) // Because it starts with 'G', this overlaps with g-label, but seems well worth it in this case.
if (adding) aOpt.style_add |= WS_GROUP; else aOpt.style_remove |= WS_GROUP;
else if (!stricmp(next_option, "Redraw")) // Seems a little more intuitive/memorable than "Draw".
aOpt.redraw = adding ? CONDITION_TRUE : CONDITION_FALSE; // Otherwise leave it at its default of 0.
else if (!strnicmp(next_option, "Disabled", 8))
{
// As of v1.0.26, Checked/Hidden/Disabled can be followed by an optional 1/0/-1 so that
// there is a way for a script to set the starting state by reading from an INI or registry
// entry that contains 1 or 0 instead of needing the literal word "checked" stored in there.
// Otherwise, a script would have to do something like the following before every "Gui Add":
// if Box1Enabled
// Enable = Enabled
// else
// Enable =
// Gui Add, checkbox, %Enable%, My checkbox.
if (next_option[8] && !ATOI(next_option + 8)) // If it's Disabled0, invert the mode to become "enabled".
adding = !adding;
if (aControl.hwnd) // More correct to call EnableWindow and let it set the style. Do not set the style explicitly in this case since that might break it.
if (adding) aOpt.style_add |= WS_DISABLED; else aOpt.style_remove |= WS_DISABLED;
}
else if (!strnicmp(next_option, "Hidden", 6))
{
// As of v1.0.26, Checked/Hidden/Disabled can be followed by an optional 1/0/-1 so that
// there is a way for a script to set the starting state by reading from an INI or registry
// entry that contains 1 or 0 instead of needing the literal word "checked" stored in there.
// Otherwise, a script would have to do something like the following before every "Gui Add":
// if Box1Enabled
// Enable = Enabled
// else
// Enable =
// Gui Add, checkbox, %Enable%, My checkbox.
if (next_option[6] && !ATOI(next_option + 6)) // If it's Hidden0, invert the mode to become "show".
adding = !adding;
if (aControl.hwnd) // More correct to call ShowWindow() and let it set the style. Do not set the style explicitly in this case since that might break it.
if (adding) aOpt.style_remove |= WS_VISIBLE; else aOpt.style_add |= WS_VISIBLE;
}
else if (!stricmp(next_option, "Wrap"))
{
switch(aControl.type)
{
case GUI_CONTROL_TEXT: // This one is a little tricky but the below should be appropriate in most cases:
if (adding) aOpt.style_remove |= SS_TYPEMASK; else aOpt.style_add = (aOpt.style_add & ~SS_TYPEMASK) | SS_LEFTNOWORDWRAP; // v1.0.44.10: Added SS_TYPEMASK to "else" section to provide more graceful handling for cases like "-Wrap +Center", which would otherwise put an unexpected style like SS_OWNERDRAW into effect.
break;
case GUI_CONTROL_GROUPBOX:
case GUI_CONTROL_BUTTON:
case GUI_CONTROL_CHECKBOX:
case GUI_CONTROL_RADIO:
if (adding) aOpt.style_add |= BS_MULTILINE; else aOpt.style_remove |= BS_MULTILINE;
break;
case GUI_CONTROL_UPDOWN:
if (adding) aOpt.style_add |= UDS_WRAP; else aOpt.style_remove |= UDS_WRAP;
break;
case GUI_CONTROL_EDIT: // Must be a multi-line now or shortly in the future or these will have no effect.
if (adding) aOpt.style_remove |= WS_HSCROLL|ES_AUTOHSCROLL; else aOpt.style_add |= ES_AUTOHSCROLL;
// WS_HSCROLL is removed because with it, wrapping is automatically off.
break;
case GUI_CONTROL_TAB:
if (adding) aOpt.style_add |= TCS_MULTILINE; else aOpt.style_remove |= TCS_MULTILINE;
// WS_HSCROLL is removed because with it, wrapping is automatically off.
break;
// N/A for these:
//case GUI_CONTROL_PIC:
//case GUI_CONTROL_DROPDOWNLIST:
//case GUI_CONTROL_COMBOBOX:
//case GUI_CONTROL_LISTBOX:
//case GUI_CONTROL_LISTVIEW:
//case GUI_CONTROL_TREEVIEW:
//case GUI_CONTROL_DATETIME:
//case GUI_CONTROL_MONTHCAL:
//case GUI_CONTROL_HOTKEY:
//case GUI_CONTROL_SLIDER:
//case GUI_CONTROL_PROGRESS:
}
}
else if (!strnicmp(next_option, "Background", 10))
{
next_option += 10; // To help maintainability, point it to the optional suffix here.
switch(aControl.type)
{
case GUI_CONTROL_PROGRESS:
case GUI_CONTROL_LISTVIEW:
case GUI_CONTROL_TREEVIEW:
case GUI_CONTROL_STATUSBAR:
// Note that GUI_CONTROL_ATTRIB_BACKGROUND_DEFAULT and GUI_CONTROL_ATTRIB_BACKGROUND_TRANS
// don't apply to Progress or ListView controls because the window proc never receives
// CTLCOLOR messages for them.
if (adding)
{
aOpt.color_bk = ColorNameToBGR(next_option);
if (aOpt.color_bk == CLR_NONE) // A matching color name was not found, so assume it's in hex format.
// It seems strtol() automatically handles the optional leading "0x" if present:
else if (!stricmp(next_option, "Group")) // This overlaps with g-label, but seems well worth it in this case.
if (adding) aOpt.style_add |= WS_GROUP; else aOpt.style_remove |= WS_GROUP;
else if (!stricmp(next_option, "Theme"))
aOpt.use_theme = adding;
else if (!strnicmp(next_option, "Hwnd", 4))
aOpt.hwnd_output_var = g_script.FindOrAddVar(next_option + 4, 0, ALWAYS_PREFER_LOCAL); // ALWAYS_PREFER_LOCAL is debatable, but for simplicity it seems best since it causes HwndOutputVar to behave the same as the vVar option.
// Picture / ListView
else if (!strnicmp(next_option, "Icon", 4)) // Caller should ignore aOpt.icon_number if it isn't applicable for this control type.
{
next_option += 4;
if (aControl.type == GUI_CONTROL_LISTVIEW) // Unconditional regardless of the value of "adding".
aOpt.listview_view = LVS_REPORT; // Unconditional regardless of the value of "adding".
else if (!stricmp(next_option, "List"))
aOpt.listview_view = LVS_LIST; // Unconditional regardless of the value of "adding".
else if (!stricmp(next_option, "Tile")) // Fortunately, subsequent changes to the control's style do not pop it out of Tile mode. It's apparently smart enough to do that only when the LVS_TYPEMASK bits change.
{
if (g_os.IsWinXPorLater()) // Checking OS version here simplifies code in other places.
aOpt.listview_view = LV_VIEW_TILE; // LV_VIEW_TILE is compatible with LVS values such as LVS_REPORT because it doesn't overlap/conflict with them.
}
else if (aControl.type == GUI_CONTROL_LISTVIEW && !stricmp(next_option, "Hdr"))
if (adding) aOpt.style_remove |= LVS_NOCOLUMNHEADER; else aOpt.style_add |= LVS_NOCOLUMNHEADER;
else if (aControl.type == GUI_CONTROL_LISTVIEW && !strnicmp(next_option, "NoSort", 6))
{
if (!stricmp(next_option + 6, "Hdr")) // Prevents the header from being clickable like a set of buttons.
if (adding) aOpt.style_add |= LVS_NOSORTHEADER; else aOpt.style_remove |= LVS_NOSORTHEADER; // Testing shows it can't be changed after the control is created.
else // Header is still clickable (unless above is *also* specified), but has no automatic sorting.
aOpt.listview_no_auto_sort = adding;
}
else if (aControl.type == GUI_CONTROL_LISTVIEW && !stricmp(next_option, "Grid"))
if (adding) aOpt.listview_style |= LVS_EX_GRIDLINES; else aOpt.listview_style &= ~LVS_EX_GRIDLINES;
else if (!strnicmp(next_option, "Count", 5)) // Script should only provide the option for ListViews.
aOpt.limit = ATOI(next_option + 5); // For simplicity, the value of "adding" is ignored.
else if (!strnicmp(next_option, "LV", 2))
{
next_option += 2;
if (IsPureNumeric(next_option, false, false)) // Disallow whitespace in case option string ends in naked "LV".
{
DWORD given_lvstyle = ATOU(next_option); // ATOU() for unsigned.
if (adding) aOpt.listview_style |= given_lvstyle; else aOpt.listview_style &= ~given_lvstyle;
if (adding) aOpt.style_add |= CBS_SORT; else aOpt.style_remove |= CBS_SORT;
break;
}
}
// UpDown
else if (aControl.type == GUI_CONTROL_UPDOWN && !stricmp(next_option, "Horz"))
if (adding)
{
aOpt.style_add |= UDS_HORZ;
aOpt.style_add &= ~UDS_AUTOBUDDY; // Doing it this way allows "Horz +0x10" to override Horz's lack of buddy.
}
else
aOpt.style_remove |= UDS_HORZ; // But don't add UDS_AUTOBUDDY since it seems undesirable most of the time.
// Slider
else if (aControl.type == GUI_CONTROL_SLIDER && !stricmp(next_option, "Invert")) // Not called "Reverse" to avoid confusion with the non-functional style of that name.
if (adding) aControl.attrib |= GUI_CONTROL_ATTRIB_ALTBEHAVIOR; else aControl.attrib &= ~GUI_CONTROL_ATTRIB_ALTBEHAVIOR;
else if (aControl.type == GUI_CONTROL_SLIDER && !stricmp(next_option, "NoTicks"))
if (adding) aOpt.style_add |= TBS_NOTICKS; else aOpt.style_remove |= TBS_NOTICKS;
else if (aControl.type == GUI_CONTROL_SLIDER && !strnicmp(next_option, "TickInterval", 12))
{
if (adding)
{
aOpt.style_add |= TBS_AUTOTICKS;
aOpt.tick_interval = ATOI(next_option + 12);
}
else
{
aOpt.style_remove |= TBS_AUTOTICKS;
aOpt.tick_interval = -1; // Signal it to remove the ticks later below (if the window exists).
}
}
else if (!strnicmp(next_option, "Line", 4))
{
next_option += 4;
if (aControl.type == GUI_CONTROL_SLIDER)
{
if (adding)
aOpt.line_size = ATOI(next_option);
//else removal not supported.
}
else if (aControl.type == GUI_CONTROL_TREEVIEW && toupper(*next_option) == 'S')
// Seems best to consider TVS_HASLINES|TVS_LINESATROOT to be an inseparable group since
// one without the other is rare (script can always be overridden by specifying numeric styles):
if (adding) aOpt.style_add |= TVS_HASLINES|TVS_LINESATROOT; else aOpt.style_remove |= TVS_HASLINES|TVS_LINESATROOT;
}
else if (aControl.type == GUI_CONTROL_SLIDER && !strnicmp(next_option, "Page", 4))
{
if (adding)
aOpt.page_size = ATOI(next_option + 4);
//else removal not supported.
}
else if (aControl.type == GUI_CONTROL_SLIDER && !strnicmp(next_option, "Thick", 5))
{
if (adding)
{
aOpt.style_add |= TBS_FIXEDLENGTH;
aOpt.thickness = ATOI(next_option + 5);
}
else // Removing the style is enough to reset its appearance on both XP Theme and Classic Theme.
aOpt.style_remove |= TBS_FIXEDLENGTH;
}
else if (!strnicmp(next_option, "ToolTip", 7))
{
next_option += 7;
// Below was commented out because the SBARS_TOOLTIPS doesn't seem to do much, if anything.
// See bottom of BIF_StatusBar() for more comments.
//if (aControl.type == GUI_CONTROL_STATUSBAR)
//{
// if (!*next_option)
// if (adding) aOpt.style_add |= SBARS_TOOLTIPS; else aOpt.style_remove |= SBARS_TOOLTIPS;
//}
//else
if (aControl.type == GUI_CONTROL_SLIDER)
{
if (adding)
{
aOpt.tip_side = -1; // Set default.
switch(toupper(*next_option))
{
case 'T': aOpt.tip_side = TBTS_TOP; break;
case 'L': aOpt.tip_side = TBTS_LEFT; break;
case 'B': aOpt.tip_side = TBTS_BOTTOM; break;
case 'R': aOpt.tip_side = TBTS_RIGHT; break;
}
if (aOpt.tip_side < 0)
aOpt.tip_side = 0; // Restore to the value that means "use default side".
else
++aOpt.tip_side; // Offset by 1, since zero is reserved as "use default side".
aOpt.style_add |= TBS_TOOLTIPS;
}
else
aOpt.style_remove |= TBS_TOOLTIPS;
}
}
else if (aControl.type == GUI_CONTROL_SLIDER && !strnicmp(next_option, "Buddy", 5))
{
if (adding)
{
next_option += 5;
char which_buddy = *next_option;
if (which_buddy) // i.e. it's not the zero terminator
{
++next_option; // Now it should point to the variable name of the buddy control.
// Check if there's an existing *global* variable of this name. It must be global
// because the variable of a control can never be a local variable:
// It seems best to allow an input-control to lack a variable, in which case its contents will be
// lost when the form is closed (unless fetched beforehand with something like ControlGetText).
// This is because it allows layout editors and other script generators to omit the variable
// and yet still be able to generate a runnable script.
Var *candidate_var;
// ALWAYS_PREFER_LOCAL is used below so that any existing local variable (e.g. a ByRef alias or
// static) will take precedence over a global of the same name when assume-global is in effect.
// If neither type of variable exists, a global will be created if assume-global is in effect.
if ( !(candidate_var = g_script.FindOrAddVar(next_option, 0, ALWAYS_PREFER_LOCAL)) ) // Find local or global, see below.
// For now, this is always a critical error that stops the current quasi-thread rather
// than setting ErrorLevel (if ErrorLevel is called for). This is because adding a
// variable can cause one of any number of different errors to be displayed, and changing
// all those functions to have a silent mode doesn't seem worth the trouble given how
// rarely 1) a control needs to get a new variable; 2) that variable name is too long
// or not valid.
return FAIL; // It already displayed the error (e.g. name too long). Existing var (if any) is retained.
// Below: Must be a global variable since otherwise, "Gui Submit" would store its results
// in the local variables of some function that isn't even currently running. Reporting
// a runtime error seems the best way to solve this overall issue since the other
// alternatives seem overly complicated or have worse drawbacks. One alternative would
// be to do load-time resolution of vVar and store the result in the lines mAttribute.
// But in addition to the problems of parsing vVar out of the list at loadtime, something
// like % "v" VarContainingVar (i.e. an expression) and other things seems would introduce
// an amount of complexity at loadtime that doesn't seem worth it. Another possibility is
// to review a function's lines the first time its first "Gui Add" is encountered at runtime.
// Any local variable that match the name of the vVar global could be made into aliases so
// that they point to the global instead. But that is pretty ugly and doesn't seem worth it.
candidate_var = candidate_var->ResolveAlias(); // Update it to its target if it's an alias. This might be relied upon by Gui::FindControl() and other things, and also the section below.
if (candidate_var->IsNonStaticLocal()) // Note that an alias can point to a local vs. global var.
return g_script.ScriptError("A control's variable must be global or static." ERR_ABORT, next_option - 1);
// Another reason that the above always resolves aliases is because it allows the next
// check below to find true duplicates, even if different aliases are used to create the
// controls (i.e. if two alias both point to the same global).
// Check if any other control (visible or not, to avoid the complexity of a hidden control
// needing to be dupe-checked every time it becomes visible) on THIS gui window has the
// same variable. That's an error because not only doesn't it make sense to do that,
// but it might be useful to uniquely identify a control by its variable name (when making
// changes to it, etc.) Note that if this is the first control being added, mControlCount
// is now zero because this control has not yet actually been added. That is why
: g_script.ScriptError("The same variable cannot be used for more than one control." // It used to say "one control per window" but that seems more confusing than it's worth.
ERR_ABORT, next_option - 1);
aControl.output_var = candidate_var;
break;
case 'E': // Extended style
if (IsPureNumeric(next_option, false, false)) // Disallow whitespace in case option string ends in naked "E".
{
// Pure numbers are assumed to be style additions or removals:
DWORD given_exstyle = ATOU(next_option); // ATOU() for unsigned.
if (adding) aOpt.exstyle_add |= given_exstyle; else aOpt.exstyle_remove |= given_exstyle;
}
break;
case 'C': // Color
if (aControl.type == GUI_CONTROL_PIC) // Don't corrupt the union's hbitmap member.
break;
COLORREF new_color;
new_color = ColorNameToBGR(next_option);
if (new_color == CLR_NONE) // A matching color name was not found, so assume it's in hex format.
// It seems strtol() automatically handles the optional leading "0x" if present:
// if next_option did not contain something hex-numeric, black (0x00) will be assumed,
// which seems okay given how rare such a problem would be.
if (color_main != new_color || &color_main == &aOpt.color_listview) // Always indicate that it changed if it's not a stored attribute of the control (so that cDefault can be detected).
{
color_main = new_color; // color_main is a reference to the right struct member.
aOpt.color_changed = true;
}
break;
case 'W':
if (toupper(*next_option) == 'P') // Use the previous control's value.
aOpt.width = mPrevWidth + ATOI(next_option + 1);
else
aOpt.width = ATOI(next_option);
break;
case 'H':
if (toupper(*next_option) == 'P') // Use the previous control's value.
DWORD new_style = (current_style | aOpt.style_add) & ~aOpt.style_remove; // Some things such as GUI_CONTROL_TEXT+SS_TYPEMASK might rely on style_remove being applied *after* style_add.
// Fix for v1.0.24:
// Certain styles can't be applied with a simple bit-or. The below section is a subset of
// a similar section in AddControl() to make sure that these styles are propertly handled:
switch (aControl.type)
{
case GUI_CONTROL_PIC:
// Fixed for v1.0.25.11 to prevent SS_ICON from getting changed to SS_BITMAP:
new_style = (new_style & ~0x0F) | (current_style & 0x0F); // Done to ensure the lowest four bits are pure.
break;
case GUI_CONTROL_GROUPBOX:
// There doesn't seem to be any flexibility lost by forcing the buttons to be the right type,
// and doing so improves maintainability and peace-of-mind:
new_style = (new_style & ~BS_TYPEMASK) | BS_GROUPBOX; // Force it to be the right type of button.
break;
case GUI_CONTROL_BUTTON:
if (new_style & BS_DEFPUSHBUTTON)
new_style = (new_style & ~BS_TYPEMASK) | BS_DEFPUSHBUTTON; // Done to ensure the lowest four bits are pure.
else
new_style &= ~BS_TYPEMASK; // Force it to be the right type of button --> BS_PUSHBUTTON == 0
// Fixed for v1.0.33.01:
// The following must be done here rather than later because it's possible for the
// button to lack the BS_DEFPUSHBUTTON style even when it is the default button (such as when
// keyboard focus is on some other button). Consequently, no difference in style might be
// detected further below, which is why it's done here:
if (aControlIndex == mDefaultButtonIndex)
{
if (aOpt.style_remove & BS_DEFPUSHBUTTON)
{
// Remove the default button (rarely needed so that's why there is current no
// "Gui, NoDefaultButton" command:
mDefaultButtonIndex = -1;
// This will alter the control id received via WM_COMMAND when the user presses ENTER:
SendMessage(mHwnd, DM_SETDEFID, (WPARAM)IDOK, 0); // restore to default
// Sometimes button visually has the default style even when GetWindowLong() says
// it doesn't. Given how rare it is to not have a default button at all after having
// one, rather than try to analyze exactly what circumstances this happens in,
// unconditionally reset the style and redraw by indicating that current style is
// different from the new style:
current_style |= BS_DEFPUSHBUTTON;
}
}
else // This button isn't the default button yet, but is it becoming it?
{
// Remember that the default button doesn't always have the BS_DEFPUSHBUTTON, namely at
// times when it shouldn't visually appear to be the default, such as when keyboard focus
// is on some other button. Therefore, don't rely on new_style or current_style's
// having or not having BS_DEFPUSHBUTTON as being a correct indicator.
if (aOpt.style_add & BS_DEFPUSHBUTTON)
{
// First remove the style from the old default button, if there is one:
// This button's visual/default appearance will be updated further below, if warranted.
}
}
break;
case GUI_CONTROL_CHECKBOX:
// Note: BS_AUTO3STATE and BS_AUTOCHECKBOX are mutually exclusive due to their overlap within
// the bit field:
if ((new_style & BS_AUTO3STATE) == BS_AUTO3STATE) // Fixed for v1.0.45.03 to check if all the BS_AUTO3STATE bits are present, not just "any" of them. BS_TYPEMASK is not involved here because this is a purity check, and TYPEMASK would defeat the whole purpose.
new_style = (new_style & ~BS_TYPEMASK) | BS_AUTO3STATE; // Done to ensure the lowest four bits are pure.
else
new_style = (new_style & ~BS_TYPEMASK) | BS_AUTOCHECKBOX; // Force it to be the right type of button.
break;
case GUI_CONTROL_RADIO:
new_style = (new_style & ~BS_TYPEMASK) | BS_AUTORADIOBUTTON; // Force it to be the right type of button.
break;
case GUI_CONTROL_DROPDOWNLIST:
new_style |= CBS_DROPDOWNLIST; // This works because CBS_DROPDOWNLIST == CBS_SIMPLE|CBS_DROPDOWN
break;
case GUI_CONTROL_COMBOBOX:
if (new_style & CBS_SIMPLE) // i.e. CBS_SIMPLE has been added to the original default, so assume it is SIMPLE.
new_style = (new_style & ~0x0F) | CBS_SIMPLE; // Done to ensure the lowest four bits are pure.
else
new_style = (new_style & ~0x0F) | CBS_DROPDOWN; // Done to ensure the lowest four bits are pure.
break;
case GUI_CONTROL_LISTVIEW: // Being in the switch serves to verify control's type because it isn't verified in places where listview_view is set.
if (aOpt.listview_view != -1) // A new view was explicitly specified.
{
// Fix for v1.0.36.04:
// For XP, must always use ListView_SetView() because otherwise switching from Tile view back to
// the *same* view that was in effect prior to tile view wouldn't work (since the the control's
// style LVS_TYPEMASK bits would not have changed). This is because LV_VIEW_TILE is a special
if (aOpt.redraw == CONDITION_TRUE // Since redrawing is being turned back on, invalidate the control so that it updates itself.
&& aControl.type != GUI_CONTROL_TREEVIEW) // This type is documented not to need it; others like ListView are not, so might need it on some OSes or under some conditions.
do_invalidate_rect = true;
}
if (do_invalidate_rect)
InvalidateRect(aControl.hwnd, NULL, TRUE); // Assume there's text in the control.
if (style_needed_changing && !style_change_ok) // Override the default errorlevel set by our caller, GuiControl().
// Not done as class to avoid code-size overhead of initializer list, etc.
{
ZeroMemory(&aOpt, sizeof(GuiControlOptionsType));
if (aControl.type == GUI_CONTROL_LISTVIEW) // Since this doesn't have the _add and _remove components, must initialize.
{
if (aControl.hwnd)
aOpt.listview_style = ListView_GetExtendedListViewStyle(aControl.hwnd); // Will have no effect on 95/NT4 that lack comctl32.dll 4.70+ distributed with MSIE 3.x
aOpt.listview_view = -1; // Indicate "unspecified" so that changes can be detected.
SendMessage(aControl.hwnd, msg_select, (WPARAM)item_index, 0); // Select this item.
}
++next_field; // Now this could be a third mDelimiter, which would in effect be an empty item.
// It can also be the zero terminator if the list ends in a delimiter, e.g. item1|item2||
}
}
this_field = next_field;
} // for()
if (aControl.type == GUI_CONTROL_LISTVIEW)
{
// Fix for v1.0.36.03: requested_index is already one beyond the number of columns that were added
// because it's always set up for the next column that would be added if there were any.
// Therefore, there is no need to add one to it to get the column count.
aControl.union_lv_attrib->col_count = requested_index; // Keep track of column count, mostly so that LV_ModifyCol and such can properly maintain the array of columns).
// It seems a useful default to do a basic auto-size upon creation, even though it won't take into
// account contents of the rows or the later presence of a vertical scroll bar. The last column
// will be overlapped when/if a v-scrollbar appears, which would produce an h-scrollbar too.
// It seems best to retain this behavior rather than trying to shrink the last column to allow
// room for a scroll bar because: 1) Having it use all available width is desirable at least
// some of the time (such as times when there will be only a few rows; 2) It simplifies the code.
// This method of auto-sizing each column to fit its text works much better than setting
// lvc.cx to ListView_GetStringWidth upon creation of the column.
if (ControlGetListViewMode(aControl.hwnd) == LVS_REPORT)
for (int i = 0; i < requested_index; ++i) // Auto-size each column.
ListView_SetColumnWidth(aControl.hwnd, i, LVSCW_AUTOSIZE_USEHEADER);
}
// Have aChoice take precedence over any double-piped item(s) that appeared in the list:
if (aChoice < 1)
return;
--aChoice;
if (aControl.type == GUI_CONTROL_TAB)
// MSDN: "A tab control does not send a TCN_SELCHANGING or TCN_SELCHANGE notification message
// when a tab is selected using the TCM_SETCURSEL message."
TabCtrl_SetCurSel(aControl.hwnd, aChoice);
else if (msg_select == LB_SETSEL) // Multi-select box requires diff msg to have a cumulative effect.
// There is evidence that SW_SHOWNORMAL might be better than SW_SHOW for the first showing because
// someone reported that a window appears centered on the screen for its first showing even if some
// other position was specified. In addition, MSDN says (without explanation): "An application should
// specify [SW_SHOWNORMAL] when displaying the window for the first time." However, SW_SHOWNORMAL is
// avoided after the first showing of the window because that would probably also do a "restore" on the
// window if it was maximized previously. Note that the description of SW_SHOWNORMAL is virtually the
// same as that of SW_RESTORE in MSDN. UPDATE: mGuiShowHasNeverBeenDone is used here instead of mFirstActivation
// because it seems more flexible to have "Gui Show" behave consistently (SW_SHOW) every time after
// the first use of "Gui Show". UPDATE: Since SW_SHOW seems to have no effect on minimized windows,
// at least on XP, and since such a minimized window will be restored by action of SetForegroundWindowEx(),
// it seems best to unconditionally use SW_SHOWNORMAL, rather than "mGuiShowHasNeverBeenDone ? SW_SHOWNORMAL : SW_SHOW".
// This is done so that the window will be restored and thus have a better chance of being successfully
// activated (and thus not requiring the call to SetForegroundWindowEx()).
// v1.0.44.08: Fixed to default to SW_SHOW for currently-maximized windows so that they don't get unmaximized
// by "Gui Show" (unless other options require it). Also, it's been observed that SW_SHOWNORMAL differs from
// SW_RESTORE in at least one way: When the target is a minimized window, SW_SHOWNORMAL will both restore it
// and unmaximize it. But SW_RESTORE unminimizes the window in a way that retains its maximized state (if
// it was previously maximized). Therefore, SW_RESTORE is now the default if the window is currently minimized.
if (is_minimized)
show_mode = SW_RESTORE; // See above comments. For backward compatibility, window is unminimized even if it was previously hidden (rather than simply showing its taskbar button and keeping it minimized).
else if (is_maximized)
show_mode = SW_SHOW; // See above.
else
show_mode = SW_SHOWNORMAL;
for (char *cp = aOptions; *cp; ++cp)
{
switch(toupper(*cp))
{
// For options such as W, H, X and Y: Use atoi() vs. ATOI() to avoid interpreting something like 0x01B
// as hex when in fact the B was meant to be an option letter.
case 'A':
if (!strnicmp(cp, "AutoSize", 8))
{
// Skip over the text of the name so that it isn't interpreted as option letters.
// 7 vs. 8 to avoid the loop's addition ++cp from reading beyond the length of the string:
cp += 7;
auto_size = true;
}
break;
case 'C':
if (!strnicmp(cp, "Center", 6))
{
// Skip over the text of the name so that it isn't interpreted as option letters.
// 5 vs. 6 to avoid the loop's addition ++cp from reading beyond the length of the string:
cp += 5;
x = COORD_CENTERED;
y = COORD_CENTERED;
// If the window is currently maximized, show_mode isn't set to SW_RESTORE unconditionally here
// due to obscurity and because it might reduce flexibility. If the window is currently minimized,
// above has already set the default show_mode to SW_RESTORE to ensure correct operation of
// something like "Gui, Show, Center".
}
break;
case 'M':
if (!strnicmp(cp, "Minimize", 8)) // Seems best to reserve "Min" for other things, such as Min W/H. "Minimize" is also more self-documenting.
{
// Skip over the text of the name so that it isn't interpreted as option letters.
// 7 vs. 8 to avoid the loop's addition ++cp from reading beyond the length of the string:
cp += 7;
show_mode = SW_MINIMIZE; // Seems more typically useful/desirable than SW_SHOWMINIMIZED.
}
else if (!strnicmp(cp, "Maximize", 8))
{
// Skip over the text of the name so that it isn't interpreted as option letters.
// 7 vs. 8 to avoid the loop's addition ++cp from reading beyond the length of the string:
width = rect.right - rect.left; // rect.left might be slightly less than zero.
height = rect.bottom - rect.top; // rect.top might be slightly less than zero. A status bar is properly handled since it's inside the window's client area.
RECT work_rect;
SystemParametersInfo(SPI_GETWORKAREA, 0, &work_rect, 0); // Get desktop rect excluding task bar.
int work_width = work_rect.right - work_rect.left; // Note that "left" won't be zero if task bar is on left!
int work_height = work_rect.bottom - work_rect.top; // Note that "top" won't be zero if task bar is on top!
// Seems best to restrict window size to the size of the desktop whenever explicit sizes
// weren't given, since most users would probably want that. But only on first use of
// "Gui Show" (even "Gui, Show, Hide"):
if (mGuiShowHasNeverBeenDone)
{
if (width_orig == COORD_UNSPECIFIED && width > work_width)
width = work_width;
if (height_orig == COORD_UNSPECIFIED && height > work_height)
height = work_height;
}
if (x == COORD_CENTERED || y == COORD_CENTERED) // Center it, based on its dimensions determined above.
{
// This does not currently handle multi-monitor systems explicitly, since those calculations
// require API functions that don't exist in Win95/NT (and thus would have to be loaded
// dynamically to allow the program to launch). Therefore, windows will likely wind up
// being centered across the total dimensions of all monitors, which usually results in
// half being on one monitor and half in the other. This doesn't seem too terrible and
// might even be what the user wants in some cases (i.e. for really big windows).
if (x == COORD_CENTERED)
x = work_rect.left + ((work_width - width) / 2);
if (y == COORD_CENTERED)
y = work_rect.top + ((work_height - height) / 2);
}
RECT old_rect;
GetWindowRect(mHwnd, &old_rect);
int old_width = old_rect.right - old_rect.left;
int old_height = old_rect.bottom - old_rect.top;
// Avoid calling MoveWindow() if nothing changed because it might repaint/redraw even if window size/pos
// didn't change:
if (width != old_width || height != old_height || (x != COORD_UNSPECIFIED && x != old_rect.left)
|| (y != COORD_UNSPECIFIED && y != old_rect.top)) // v1.0.45: Fixed to be old_rect.top not old_rect.bottom.
{
// v1.0.44.08: Window state gets messed up if it's resized without first unmaximizing it (for example,
// it can't be resized by dragging its lower-right corner). So it seems best to unmaximize, perhaps
// even when merely the position is changing rather than the size (even on a multimonitor system,
// it might not be valid to reposition a maximized window without unmaximizing it?)
if (IsZoomed(mHwnd)) // Call IsZoomed() again in case above changed the state. No need to check IsIconic() because above already set default show-mode to SW_RESTORE for such windows.
ShowWindow(mHwnd, SW_RESTORE); // But restore isn't done for something like "Gui, Show, Center" because it's too obscure and might reduce flexibility (debatable).
MoveWindow(mHwnd, x == COORD_UNSPECIFIED ? old_rect.left : x, y == COORD_UNSPECIFIED ? old_rect.top : y
, width, height, is_visible); // Do repaint if window is visible.
}
// Added for v1.0.44.13:
// Below is done inside this block (allow_move_window) because it that way, it should always
// execute whenever mGuiShowHasNeverBeenDone (since the window shouldn't be iconic prior to
// its first showing). In additon, below must be down prior to any ShowWindow() that does
// a minimize or maximize because that would prevent GetWindowRect/GetClientRect calculations
// below from working properly.
if (mGuiShowHasNeverBeenDone) // This is the first showing of this window.
{
// Now that the window's style, edge type, title bar, menu bar, and other non-client attributes have
// likely (but not certainly) been determined, adjust MinMaxSize values from client size to
// entire-window size for use with WM_GETMINMAXINFO.
// To help reduce code size, the following isn't done (the calls later below are probably very fast):
// Caller has ensured that aGuiWindowIndex is less than MAX_GUI_WINDOWS.
// We're returning the length of the var's contents, not the size.
{
GuiType *pgui;
// Relies on short-circuit boolean order:
if (aControlIndex >= MAX_CONTROLS_PER_GUI // Must check this first due to short-circuit boolean. A non-GUI thread or one triggered by GuiClose/Escape or Gui menu bar.
|| !(pgui = g_gui[aGuiWindowIndex]) // Gui Window no longer exists.
|| aControlIndex >= pgui->mControlCount) // Gui control no longer exists, perhaps because window was destroyed and recreated with fewer controls.
{
if (aBuf)
*aBuf = '\0';
return 0;
}
GuiControlType &control = pgui->mControl[aControlIndex]; // For performance and convenience.
if (aBuf)
{
// Caller has already ensured aBuf is large enough.
if (GetWindowLong(aControl.hwnd, GWL_STYLE) & MCS_MULTISELECT)
{
// For code simplicity and due to the expected rarity of using the MonthCal control, much less
// in its range-select mode, the range is returned with a dash between the min and max rather
// than as an array or anything fancier.
MonthCal_GetSelRange(aControl.hwnd, st);
// Seems easier for script (due to consistency) to always return it in range format, even if
// only one day is selected.
SystemTimeToYYYYMMDD(buf, st[0]);
buf[8] = '-'; // Retain only the first 8 chars to omit the time portion, which is unreliable (not relevant anyway).
SystemTimeToYYYYMMDD(buf + 9, st[1]);
return aOutputVar.Assign(buf, 17); // Limit to 17 chars to omit the time portion of the second timestamp.
}
else
{
MonthCal_GetCurSel(aControl.hwnd, st);
return aOutputVar.Assign(SystemTimeToYYYYMMDD(buf, st[0]), 8); // Limit to 8 chars to omit the time portion, which is unreliable (not relevant anyway).
}
case GUI_CONTROL_HOTKEY:
// Testing shows that neither GetWindowText() nor WM_GETTEXT can pull anything out of a hotkey
// control, so the only type of retrieval that can be offered is the HKM_GETHOTKEY method:
// Find the index of the control that matches the string, which can be either:
// 1) The name of a control's associated output variable.
// 2) Class+NN
// 3) Control's title/caption.
// Returns -1 if not found.
{
// v1.0.44.08: Added the following check. Without it, ControlExist() (further below) would retrieve the
// topmost child, which isn't very useful or intuitive. This currently affects only the following commands:
// 1) GuiControl (but not GuiControlGet because it has special handling for a blank ControlID).
// 2) Gui, ListView|TreeView, MyTree|MyList
if (!*aControlID)
return -1;
GuiIndexType u;
// To keep things simple, the first search method is always conducted: It looks for a
// matching variable name, but only among the variables used by this particular window's
// controls (i.e. avoid ambiguity by NOT having earlier matched up aControlID against
// all variable names in the entire script, perhaps in PreparseBlocks() or something).
// UPDATE: For v1.0.31, the performance is improved by resolving the variable to its
// pointer first, rather than comparing the variable names for a match. It's further
// improved by skipping the first loop entirely when aControlID doesn't exist as a global
// variable (GUI controls always have global variables, not locals).
Var *var;
if (var = g_script.FindVar(aControlID, 0, NULL, ALWAYS_USE_GLOBAL)) // First search globals only because for backward compatibility, a GUI control whose Var* is identical to that of a global should be given precedence over a static that matches some other control. Furthermore, since most GUI variables are global, doing this check before the static check improves avg-case performance.
{
// No need to do "var = var->ResolveAlias()" because the line above never finds locals, only globals.
// Similarly, there's no need to do confirm that var->IsLocal()==false.
for (u = 0; u < mControlCount; ++u)
if (mControl[u].output_var == var)
return u; // Match found.
}
if (g.CurrentFunc // v1.0.46.15: Since above failed to match: if we're in a function (which is checked for performance reasons), search for a static or ByRef-that-points-to-a-global-or-static because both should be supported.
int font_index = FindOrCreateFont(aOptions, aFontName, &sFont[mCurrentFontIndex], &color);
if (color != CLR_NONE) // Even if the above call failed, it returns a color if one was specified.
mCurrentColor = color;
if (font_index > -1) // Success.
{
mCurrentFontIndex = font_index;
return OK;
}
// Failure of the above is rare because it falls back to default typeface if the one specified
// isn't found. It will have already displayed the error:
return FAIL;
}
int GuiType::FindOrCreateFont(char *aOptions, char *aFontName, FontType *aFoundationFont, COLORREF *aColor)
// Returns the index of existing or new font within the sFont array (an index is returned so that
// caller can see that this is the default-gui-font whenever index 0 is returned). Returns -1
// on error, but still sets *aColor to be the color name, if any was specified in aOptions.
// To prevent a large number of font handles from being created (such as one for each control
// that uses something other than GUI_DEFAULT_FONT), it seems best to conserve system resources
// by creating new fonts only when called for. Therefore, this function will first check if
// the specified font already exists within the array of fonts. If not found, a new font will
// be added to the array.
{
// Set default output parameter in case of early return:
if (aColor) // Caller wanted color returned in an output parameter.
*aColor = CLR_NONE; // Because we want CLR_DEFAULT to indicate a real color.
HDC hdc;
if (!*aOptions && !*aFontName)
{
// Relies on the fact that first item in the font array is always the default font.
// If there are fonts, the default font should be the first one (index 0).
// If not, we create it here:
if (!sFontCount)
{
// For simplifying other code sections, create an entry in the array for the default font
// (GUI constructor relies on at least one font existing in the array).
if (!sFont) // v1.0.44.14: Created upon first use to conserve ~14 KB memory in non-GUI scripts.
if ( !(sFont = (FontType *)malloc(sizeof(FontType) * MAX_GUI_FONTS)) )
g_script.ExitApp(EXIT_CRITICAL, ERR_OUTOFMEM); // Since this condition is so rare, just abort to avoid the need to add extra logic in several places to detect a failed/NULL array.
// Doesn't seem likely that DEFAULT_GUI_FONT face/size will change while a script is running,
// or even while the system is running for that matter. I think it's always an 8 or 9 point
// font regardless of desktop's appearance/theme settings.
ZeroMemory(&sFont[sFontCount], sizeof(FontType));
// SYSTEM_FONT seems to be the bold one that is used in a dialog window by default.
// MSDN: "It is not necessary (but it is not harmful) to delete stock objects by calling DeleteObject."
g_script.ScriptError("Can't create font." ERR_ABORT); // Short msg since so rare.
return -1;
}
sFont[sFontCount++] = font; // Copy the newly created font's attributes into the next array element.
return sFontCount - 1; // The index of the newly created font.
}
int GuiType::FindFont(FontType &aFont)
{
for (int i = 0; i < sFontCount; ++i)
if (!stricmp(sFont[i].name, aFont.name) // lstrcmpi() is not used: 1) avoids breaking exisitng scripts; 2) provides consistent behavior across multiple locales.
&& sFont[i].point_size == aFont.point_size
&& sFont[i].weight == aFont.weight
&& sFont[i].italic == aFont.italic
&& sFont[i].underline == aFont.underline
&& sFont[i].strikeout == aFont.strikeout) // Match found.
// If a message pump other than our own is running -- such as that of a dialog like MsgBox -- it will
// dispatch messages directly here. This is detected by means of g.CalledByIsDialogMessageOrDispatch==false.
// Such messages need to be checked here because MsgSleep hasn't seen the message and thus hasn't
// done the check. The g.CalledByIsDialogMessageOrDispatch method relies on the fact that we never call
// MsgSleep here for the types of messages dispatched from MsgSleep, which seems true. Also, if
// we do lauch a monitor thread here via MsgMonitor, that means g.CalledByIsDialogMessageOrDispatch==false.
// Therefore, any calls to MsgSleep made by the new thread can't corrupt our caller's settings of
// g.CalledByIsDialogMessageOrDispatch because in that case, our caller isn't MsgSleep's IsDialog/Dispatch.
// As an added precaution against the complexity of these message issues (only one of several such scenarios
// is described above), CalledByIsDialogMessageOrDispatch is put into the g-struct rather than being
// a normal global. That way, a thread's calls to MsgSleep can't interfere with the value of
// CalledByIsDialogMessageOrDispatch for any threads beneath it. Although this may technically be
// unnecessary, it adds maintainability.
LRESULT msg_reply;
if (g_MsgMonitorCount // Count is checked here to avoid function-call overhead.
&& (!g.CalledByIsDialogMessageOrDispatch || g.CalledByIsDialogMessageOrDispatchMsg != iMsg) // v1.0.44.11: If called by IsDialog or Dispatch but they changed the message number, check if the script is monitoring that new number.
return msg_reply; // MsgMonitor has returned "true", indicating that this message should be omitted from further processing.
g.CalledByIsDialogMessageOrDispatch = false;
// Fixed for v1.0.40.01: The above line was added to resolve a case where our caller did make the value
// true but the message it sent us results in a recursive call to us (such as when the user resizes a
// window by dragging its borders: that apparently starts a loop in DefDlgProc that calls this
// function recursively). This fixes OnMessage(0x24, "WM_GETMINMAXINFO") and probably others.
// Known limitation: If the above launched a thread but the thread didn't cause it turn return,
// and iMsg is something like AHK_GUI_ACTION that will be reposted via PostMessage(), the monitor
// will be launched again when MsgSleep is called in conjunction with the repost. Given the rarity
// and the minimal consequences of this, no extra code (such as passing a new parameter to MsgSleep)
// is added to handle this.
GuiType *pgui;
GuiControlType *pcontrol;
GuiIndexType control_index;
RECT rect;
bool text_color_was_changed;
char buf[1024];
switch (iMsg)
{
// case WM_CREATE: --> Do nothing extra becuase DefDlgProc() appears to be sufficient.
case WM_SIZE: // Listed first for performance.
if ( !(pgui = GuiType::FindGui(hWnd)) )
break; // Let default proc handle it.
if (pgui->mStatusBarHwnd)
// Send the msg even if the bar is hidden because the OS typically knows not to do extra drawing work for
// hidden controls. In addition, when the bar is shown again, it might be the wrong size if this isn't done.
// Known/documented limitation: In spite of being in the right z-order position, any control that
// overlaps the status bar might sometimes get drawn on top of it.
SendMessage(pgui->mStatusBarHwnd, WM_SIZE, wParam, lParam); // It apparently ignores wParam and lParam, but just in case send it the actuals.
// Note that SIZE_MAXSHOW/SIZE_MAXHIDE don't seem to ever be received under the conditions
// described at MSDN, even if the window has WS_POPUP style. Therefore, A_EventInfo will
// probably never contain those values, and as a result they are not documented in the help file.
if (pgui->mLabelForSize) // There is an event handler in the script.
POST_AHK_GUI_ACTION(hWnd, LOWORD(wParam), GUI_EVENT_RESIZE, lParam); // LOWORD(wParam) just to be sure it fits in 16-bit, but SIZE_MAXIMIZED and the others all do.
// MsgSleep() is not done because "case AHK_GUI_ACTION" in GuiWindowProc() takes care of it.
// See its comments for why.
return 0; // "If an application processes this message, it should return zero."
// Testing shows that the window still resizes correctly (controls are revealed as the window
// is expanded) even if the event isn't passed on to the default proc.
case WM_GETMINMAXINFO: // Added for v1.0.44.13.
{
if ( !(pgui = GuiType::FindGui(hWnd)) )
break; // Let default proc handle it.
MINMAXINFO &mmi = *(LPMINMAXINFO)lParam;
if (pgui->mMinWidth >= 0) // This check covers both COORD_UNSPECIFIED and COORD_CENTERED.
mmi.ptMinTrackSize.x = pgui->mMinWidth;
if (pgui->mMinHeight >= 0)
mmi.ptMinTrackSize.y = pgui->mMinHeight;
if (pgui->mMaxWidth >= 0) // mmi.ptMaxSize.x/y aren't changed because it seems the OS
mmi.ptMaxTrackSize.x = pgui->mMaxWidth; // automatically uses ptMaxTrackSize for them, at least when
if (pgui->mMaxHeight >= 0) // ptMaxTrackSize is smaller than the system's default for
return 0; // "If an application processes this message, it should return zero."
}
case WM_COMMAND:
{
// First find which of the GUI windows is receiving this event:
if ( !(pgui = GuiType::FindGui(hWnd)) )
break; // No window (might be impossible since this function is for GUI windows, but seems best to let DefDlgProc handle it).
int id = LOWORD(wParam);
// For maintainability, this is checked first because "code" (the HIWORD) is sometimes or always 0,
// which falsely indicates that the message is from a menu:
if (id == IDCANCEL) // IDCANCEL is a special Control ID. The user pressed esc.
{
// Known limitation:
// Example:
//Gui, Add, Text,, Gui1
//Gui, Add, Text,, Gui2
//Gui, Show, w333
//GuiControl, Disable, Gui1
//return
//
//GuiEscape:
//MsgBox GuiEscape
//return
// It appears that in cases like the above, the OS doesn't send the WM_COMMAND+IDCANCEL message
// to the program when you press Escape. Although it could be fixed by having the escape keystroke
// unconditionally call the GuiEscape label, that might break existing features and scripts that
// rely on escape's ability to perform other functions in a window.
// I'm not sure whether such functions exist and how many of them there are. Examples might include
// using escape to close a menu, drop-list, or other pop-up attribute of a control inside the window.
// Escape also cancels a user's drag-move of the window (restoring the window to its pre-drag location).
// If pressing escape were to unconditionally call the GuiEscape label, features like these might be
// broken. So currently this behavior is documented in the help file as a known limitation.
pgui->Escape();
return 0; // Might be necessary to prevent auto-window-close.
// Note: It is not necessary to check for IDOK because:
// 1) If there is no default button, the IDOK message is ignored.
// 2) If there is a default button, we should never receive IDOK because BM_SETSTYLE (sent earlier)
// will have altered the message we receive to be the ID of the actual default button.
}
// Since above didn't return:
if (id >= ID_USER_FIRST)
{
// Since all control id's are less than ID_USER_FIRST, this message is either
// a user defined menu item ID or a bogus message due to it corresponding to
// a non-existent menu item or a main/tray menu item (which should never be
// received or processed here).
HandleMenuItem(hWnd, id, pgui->mWindowIndex);
return 0; // Indicate fully handled.
}
// Otherwise id should contain the ID of an actual control. Validate that in case of bogus msg.
// Perhaps because this is a DialogProc rather than a WindowProc, the following does not appear
// to be true: MSDN: "The high-order word [of wParam] specifies the notification code if the message
// is from a control. If the message is from an accelerator, [high order word] is 1. If the message
// is from a menu, [high order word] is zero."
GuiIndexType control_index = GUI_ID_TO_INDEX(id); // Convert from ID to array index. Relies on unsigned to flag as out-of-bounds.
if (control_index < pgui->mControlCount // Relies on short-circuit boolean order.
&& pgui->mControl[control_index].hwnd == (HWND)lParam) // Handles match (this filters out bogus msgs).
pgui->Event(control_index, HIWORD(wParam));
// v1.0.35: And now pass it on to DefDlgProc() in case it needs to see certain types of messages.
break;
}
case WM_SYSCOMMAND:
if (wParam == SC_CLOSE)
{
if ( !(pgui = GuiType::FindGui(hWnd)) )
break; // Let DefDlgProc() handle it.
pgui->Close();
return 0;
}
break;
case WM_NOTIFY:
{
if ( !(pgui = GuiType::FindGui(hWnd)) )
break; // Let DefDlgProc() handle it.
NMHDR &nmhdr = *(LPNMHDR)lParam;
control_index = (GuiIndexType)GUI_ID_TO_INDEX(nmhdr.idFrom); // Convert from ID to array index. Relies on unsigned to flag as out-of-bounds.
if (control_index >= pgui->mControlCount)
break; // Invalid to us, but perhaps meaningful DefDlgProc(), so let it handle it.
GuiControlType &control = pgui->mControl[control_index]; // For performance and convenience.
if (control.hwnd != nmhdr.hwndFrom) // Handles match (this filters out bogus msgs).
break;
UINT event_info = NO_EVENT_INFO; // Set default, to be possibly overridden below.
USHORT gui_event = '*'; // Something other than GUI_EVENT_NONE to flag events that don't get classified below. The special character helps debugging.
bool ignore_unless_alt_submit = true; // Set default, which is set to "false" only for the most important and/or rarely occuring notifications (for script performance).
switch (control.type)
{
/////////////////////
// LISTVIEW WM_NOTIFY
/////////////////////
case GUI_CONTROL_LISTVIEW:
bool is_actionable;
is_actionable = true; // Set default.
switch (nmhdr.code)
{
// MSDN: LVN_HOTTRACK: "Return zero to allow the list view to perform its normal track select processing."
// Also, LVN_HOTTRACK is listed first for performance since it arrives far more often than any other notification.
case LVN_HOTTRACK: // v1.0.36.04: No longer an event because it occurs so often: Due to single-thread limit, it was decreasing the reliability of AltSubmit ListViews' receipt of other events such as "I", such as Toralf's Icon Viewer.
case NM_CUSTOMDRAW: // Return CDRF_DODEFAULT (0). Occurs for every redraw, such as mouse cursor sliding over control or window activation.
case LVN_ITEMCHANGING: // Not yet supported (seems rarely needed), so always allow the change by returning 0 (FALSE).
case LVN_INSERTITEM: // Any ways other than ListView_InsertItem() to insert items?
case LVN_DELETEITEM: // Might be received for each individual (non-DeleteAll) deletion).
case LVN_GETINFOTIPW: // v1.0.44: Received even without LVS_EX_INFOTIP?. In any case, there's currently no point
case LVN_GETINFOTIPA: // in notifying the script because it would have no means of changing the tip (by altering the struct), except perhaps OnMessage.
return 0; // Return immediately to avoid calling Event() and DefDlgProc(). A return value of 0 is suitable for all of the above.
case 0xFFFFFF4F: // Couldn't find these in commctrl.h anywhere. They seem to occur when control is first created and once for each row in the first set of added rows.
case 0xFFFFFF5F:
case 0xFFFFFF5D: // Probably something to do with incremental search since it seems to happen only when items are present and the user types a visible-character key.
is_actionable = false;
break; // Let default proc handle them since they might mean something to it.
case LVN_ITEMCHANGED:
// This is received for selection/deselection, which means clicking a new item generates
// at least two of them (in practice, it generates between 1 and 3 but not sure why).
// It's also received for checking/unchecking an item. Extending a selection via Shift-ArrowKey
// generates between 1 and 3 of them, perhaps at random? Maybe all we can count on is that you
// get at least one when the selection has changed or a box is (un)checked.
if (control.attrib & GUI_CONTROL_ATTRIB_ALTSUBMIT) // Script asked for item-change notifications.
{
gui_event = 'I'; // Set default to be a plain I.
NMLISTVIEW &lv = *(LPNMLISTVIEW)lParam;
event_info = 1 + lv.iItem; // MSDN: If iItem is -1, the change has been applied to all items in the list view.
// Although the OS currently generates focus+select together, it sends de-focus and de-select
// separately. However, since this behavior might vary in past/future OSes, it seems best to
// use a method that will work regardless of what combinations are possible.
UINT newly_changed = lv.uNewState ^ lv.uOldState; // uChanged doesn't seem accurate: it's always 8? So derive the "correct" value of which flags have actually changed.
UINT newly_on = newly_changed & lv.uNewState;
UINT newly_off = newly_changed & lv.uOldState;
if (newly_on & LVIS_FOCUSED)
gui_event |= AHK_LV_FOCUS;
else if (newly_off & LVIS_FOCUSED)
gui_event |= AHK_LV_DEFOCUS;
if (newly_on & LVIS_SELECTED)
gui_event |= AHK_LV_SELECT;
else if (newly_off & LVIS_SELECTED)
gui_event |= AHK_LV_DESELECT;
// The following are commented out for possible future use because currently, I think they
// don't happen at all (not for dropping of files anyway). If dragging & dropping within
// a ListView or between two different ListViews ever becomes a built-in feature, this
// section (and its counterpart in the main event loop) can be re-enabled.
// In those very rare cases when a script needs LVIS_DROPHILITED, it can use OnMessage().
//if (newly_on & LVIS_DROPHILITED) // MSDN: LVIS_DROPHILITED means "the item is highlighted as a drag-and-drop target."
// gui_event |= AHK_LV_DROPHILITE;
//else if (newly_off & LVIS_DROPHILITED)
// gui_event |= AHK_LV_UNDROPHILITE;
// Below must occur only after all of the checks above:
if (newly_changed & LVIS_STATEIMAGEMASK) // State image changed.
{
if (lv.uOldState & LVIS_STATEIMAGEMASK) // Image is changing from a non-blank image to a different non-blank image.
// For simplicity, assume checkboxes are present rather than custom images.
// User can use OnMessage() to do custom handling in the rare event of having
// images other than checkboxes.
gui_event |= ((lv.uNewState & LVIS_STATEIMAGEMASK) == 0x1000) ? AHK_LV_UNCHECK : AHK_LV_CHECK; // The #1 image is "unchecked" and the #2 (or anything higher) is considered "checked".
else // State image changed from blank/none to some new image. v1.0.46.10: Omit this event because it seems to do more harm than good in 99% of cases (especially since it typically only occurs when the script calls LV_Add/Insert).
if (gui_event == 'I') // But only omit the even if there are no other changes/reasons for it.
is_actionable = false;
}
}
//else script isn't being notifid of item-changes, so leave everything uninitialized or at their
// defaults (it won't matter because further below, no event will be sent to the script).
break;
case LVN_BEGINSCROLL: gui_event = 'S'; break;
case LVN_ENDSCROLL: gui_event = 's'; break; // Lowercase to distinguish it.
case LVN_MARQUEEBEGIN: gui_event = 'M'; break;
case NM_RELEASEDCAPTURE: gui_event = 'C'; break;
case NM_SETFOCUS: gui_event = 'F'; break;
case NM_KILLFOCUS: gui_event = 'f'; break; // Lowercase to distinguish it.
//case NM_HOVER: gui_event = 'V'; break; // Spy++ indicates that NM_HOVER is never received. Maybe a style has to be set to get it. Note: 'V' is used for Hover because 'H' is used for LVN_HOTTRACK.
//case NM_RETURN (user has pressed the ENTER key): Apparently never received, probably because the parent window uses DefDlgProc() vs. DefWindowProc().
case LVN_KEYDOWN:
// For simplicity and flexibility, it seems best to store the VK itself since it
// might not correspond to a visible character (such as a function key or modifier).
// This also helps to reduce code size since scripts will only rarely want to have
// key-down info.
gui_event = 'K';
event_info = ((LPNMLVKEYDOWN)lParam)->wVKey; // The one-based column number that was clicked.
if (event_info == VK_F2 && !(control.attrib & GUI_CONTROL_ATTRIB_ALTBEHAVIOR)) // WantF2 is in effect.
{
int focused_index = ListView_GetNextItem(control.hwnd, -1, LVNI_FOCUSED);
if (focused_index != -1)
SendMessage(control.hwnd, LVM_EDITLABEL, focused_index, 0); // Has no effect if the control is read-only.
// For flexibility, it seems to still notify the script of the F2 keystroke in case
// it wants to do extra things. Testing shows that even if the script sends its own
// TVM_EDITLABEL message (such as pre-1.0.44 scripts that weren't updated to take into
// account WantF2), the label still goes into edit mode properly (though it does go out
// of edit mode then back in quickly due to the duplicate message).
}
break;
// When alt-submit mode isn't in effect, it seems best to ignore all clicks except double-clicks, since
// right-click should normally be handled via GuiContenxtMenu instead (to allow AppsKey to work, etc.);
// and since left-clicks can be used to extend a selection (ctrl-click or shift-click), so are pretty
// vague events that most scripts probably wouldn't have explicit handling for. A script that needs
// to know when the selection changes can turn on AltSubmit to catch a wide variety of ways the
// selection can change, the most all-encompassing of which is probably LVN_ITEMCHANGED.
case NM_CLICK:
// v1.0.36.03: For NM_CLICK/NM_RCLICK, it's somewhat debatable to set event_info when the
// ListView isn't single-select, but the usefulness seems to outweigh any confusion it might cause.
gui_event = GUI_EVENT_NORMAL;
event_info = 1 + ListView_GetNextItem(control.hwnd, -1, LVNI_FOCUSED); // Fetch manually for compatibility with Win95/NT lacking MSIE 3.0+.
break;
case NM_RCLICK:
gui_event = GUI_EVENT_RCLK;
event_info = 1 + ListView_GetNextItem(control.hwnd, -1, LVNI_FOCUSED); // Fetch manually for compatibility with Win95/NT lacking MSIE 3.0+.
break;
case NM_DBLCLK:
gui_event = GUI_EVENT_DBLCLK;
event_info = 1 + ListView_GetNextItem(control.hwnd, -1, LVNI_FOCUSED); // Fetch manually for compatibility with Win95/NT lacking MSIE 3.0+.
ignore_unless_alt_submit = false;
break;
case NM_RDBLCLK:
gui_event = 'R'; // Rare, so just a simple mnemonic is stored (seems better than a digit).
event_info = 1 + ListView_GetNextItem(control.hwnd, -1, LVNI_FOCUSED); // Fetch manually for compatibility with Win95/NT lacking MSIE 3.0+.
ignore_unless_alt_submit = false;
break;
case LVN_ITEMACTIVATE: // By default, this notification arrives when an item is double-clicked (depends on style).
gui_event = 'A';
event_info = 1 + ListView_GetNextItem(control.hwnd, -1, LVNI_FOCUSED); // Fetch manually for compatibility with Win95/NT lacking MSIE 3.0+.
break;
case LVN_COLUMNCLICK:
{
gui_event = GUI_EVENT_COLCLK;
NMLISTVIEW &lv = *(LPNMLISTVIEW)lParam;
event_info = 1 + lv.iSubItem; // The one-based column number that was clicked.
// The following must be done here rather than in Event() in case the control has no g-label:
if (!(control.union_lv_attrib->no_auto_sort)) // Automatic sorting is in effect.
GuiType::LV_Sort(control, lv.iSubItem, true); // -1 to convert column index back to zero-based.
ignore_unless_alt_submit = false;
break;
}
case LVN_BEGINLABELEDITW: // Received even for non-Unicode apps, at least on XP. Even so, the text contained it the struct is apparently always ANSI vs. Unicode.
case LVN_BEGINLABELEDITA: // Never received, at least not on XP?
ignore_unless_alt_submit = false; // Seems best to default to notifying only after data may have been changed; plus it avoids the need for script to distinguish case of 'e' vs. 'E'.
break;
// v1.0.44: Changed drag notifications to occur in non-AltSubmit mode due to how rare drags are.
// This avoids the need for the script to turn on AltSubmit just for them.
case LVN_BEGINDRAG: // Left-drag.
gui_event = 'D';
// v1.0.44: Testing shows that the following retrieves the row upon which the use clicked, which
// in a multi-select ListView isn't necessarily the same as the focused row (which was retrieved in
// previous versions). However, due to obscurity and rarity, this is very unlikely to break any
// existing scripts and thus won't be documented as a change.
event_info = 1 + ((LPNMLISTVIEW)lParam)->iItem;
ignore_unless_alt_submit = false;
break;
case LVN_BEGINRDRAG: // Right-drag.
gui_event = 'd'; // Lowercase to distinguish it.
event_info = 1 + ((LPNMLISTVIEW)lParam)->iItem; // See comment in previous "case".
ignore_unless_alt_submit = false;
break;
case LVN_DELETEALLITEMS:
return TRUE; // For performance, tell it not to notify us as each individual item is deleted.
// After the event, explicitly return a special value for any notifications that absolutely
// require it, and let default proc handle all the others.
switch (nmhdr.code)
{
case LVN_ENDLABELEDITW: // Received even for non-Unicode apps, at least on XP. Even so, the text contained it the struct is apparently always ANSI vs. Unicode.
case LVN_ENDLABELEDITA: // Never received, at least not on XP?
// MSDN: "If the pszText member of the LVITEM structure is NULL, the return value is ignored."
// Therefore, returning TRUE to allow the edit should be the correct value in every case, at
// least until such time as the ability for a script to override individual edits is provided.
return TRUE; // Must return TRUE explicitly because apparently DefDlgProc() would return FALSE.
}
break; // Let default proc handle them all in case it does any extra processing.
/////////////////////
// TREEVIEW WM_NOTIFY
/////////////////////
case GUI_CONTROL_TREEVIEW:
switch (nmhdr.code)
{
case NM_SETCURSOR: // Received very often, every time the mouse moves while over the control.
case NM_CUSTOMDRAW: // Return CDRF_DODEFAULT (0). Occurs for every redraw, such as mouse cursor sliding over control or window activation.
case TVN_DELETEITEMW:
case TVN_DELETEITEMA:
// TVN_SELCHANGING, TVN_ITEMEXPANDING, and TVN_SINGLEEXPAND are not reported to the script as events
// because there is currently no support for vetoing the selection-change or expansion; plus these
// notifications each have an "-ED" counterpart notification that is reported to the script (even
// TVN_SINGLEEXPAND is followed by a TVN_ITEMEXPANDED notification).
case TVN_SELCHANGINGW: // Received even for non-Unicode apps, at least on XP.
case TVN_SELCHANGINGA:
case TVN_ITEMEXPANDINGW: // Received even for non-Unicode apps, at least on XP.
case TVN_ITEMEXPANDINGA:
case TVN_SINGLEEXPAND: // Note that TVNRET_DEFAULT==0. This is received only when style contains TVS_SINGLEEXPAND.
case TVN_GETINFOTIPA: // Received when TVS_INFOTIP is present. However, there's currently no point
case TVN_GETINFOTIPW: // in notifying the script because it would have no means of changing the tip (by altering the struct), except perhaps OnMessage.
return 0; // Return immediately to avoid calling Event() and DefDlgProc(). A return value of 0 is suitable for all of the above.
case TVN_SELCHANGEDW:
case TVN_SELCHANGEDA:
// 'S' was chosen vs. 's' or 'C' because it seems easier to remember. Known drawbacks:
// - Would have to use lowercase 's' for "TVN_SELCHANGING" in case it's ever wanted (though adding
// it directly would break existing scripts that rely on case insensitivity, so it would probably be
// better to choose an entirely different letter).
// - 'S' cannot be used for scrolling notifications in case TreeView ever adds them like ListViews.
gui_event = 'S';
// Having more than one item selected in a TreeView is fairly rare due to not being meaningful or
// supported by the control. Therefore, performing a select-all on a TreeView by a script is
// likely to be uncommon, and thus the performance concern mentioned for expand-all above isn't
// as applicable. For this reason and also because selecting an item TreeView is typically of
// high interest (since eacy item may often be a folder, in which case the script changes the
// contents in a corresponding ListView), it seems best to report these in non-alt-submit mode.
// On the other hand, if a script ever does some kind of automated traversal of the Tree, selecting
// each item one at a time (probably rare), this policy would reduce performance.
case TVN_BEGINLABELEDITW: // Received even for non-Unicode apps, at least on XP. Even so, the text contained it the struct is apparently always ANSI vs. Unicode.
case TVN_BEGINLABELEDITA: // Never received, at least not on XP?
ignore_unless_alt_submit = false; // Seems best to default to notifying only after data may have been changed; plus it avoids the need for script to distinguish case of 'e' vs. 'E'.
GuiType::sTreeWithEditInProgress = NULL;
break;
case TVN_BEGINDRAGW: // Received even for non-Unicode apps, at least on XP. Even so, the text contained it the struct is apparently always ANSI vs. Unicode.
case TVN_BEGINDRAGA: // Never received, at least not on XP?
ignore_unless_alt_submit = false; // Due to how rare drags are, it seems best to report them so that AltSubmit mode doesn't have to be turned on just for them.
break;
case TVN_BEGINRDRAGW: // Same comments left-drag above.
case TVN_BEGINRDRAGA: //
gui_event = 'd'; // Right-drag. Lowercase to distinguish it.
case NM_KILLFOCUS: gui_event = 'f'; break; // Lowercase to distinguish it.
//case NM_RETURN (user has pressed the ENTER key): Apparently never received, probably because the parent window uses DefDlgProc() vs. DefWindowProc().
case TVN_KEYDOWN:
// For simplicity and flexibility, it seems best to store the VK itself since it
// might not correspond to a visible character (such as a function key or modifier).
// This also helps to reduce code size since scripts will only rarely want to have
// key-down info.
gui_event = 'K';
event_info = ((LPNMTVKEYDOWN)lParam)->wVKey; // The one-based column number that was clicked.
if (event_info == VK_F2 && !(control.attrib & GUI_CONTROL_ATTRIB_ALTBEHAVIOR)) // WantF2 is in effect.
{
HTREEITEM hitem;
if (hitem = TreeView_GetSelection(control.hwnd))
SendMessage(control.hwnd, TVM_EDITLABEL, 0, (LPARAM)hitem); // Has no effect if the control is read-only.
// For flexibility and consistency with ListView behavior, it seems to still notify the
// script of the F2 keystroke in case it wants to do extra things.
}
break;
} // switch(nmhdr.code).
// Since above didn't return, make it an event.
if (!ignore_unless_alt_submit || (control.attrib & GUI_CONTROL_ATTRIB_ALTSUBMIT))
// After the event, explicitly return a special value for any notifications that absolutely
// require it, and let default proc handle all the others.
switch (nmhdr.code)
{
case TVN_ENDLABELEDITW: // Received even for non-Unicode apps, at least on XP. Even so, the text contained it the struct is apparently always ANSI vs. Unicode.
case TVN_ENDLABELEDITA: // Never received, at least not on XP?
// MSDN: "If the pszText member is NULL, the return value is ignored."
// Therefore, returning TRUE to allow the edit should be the correct value in every case, at
// least until such time as the ability for a script to override individual edits is provided.
return TRUE; // Must return TRUE explicitly because apparently DefDlgProc() would return FALSE.
}
break; // Let default proc handle them all in case it does any extra processing.
//////////////////////
// OTHER CONTROL TYPES
//////////////////////
case GUI_CONTROL_DATETIME: // NMDATETIMECHANGE struct contains an NMHDR as it's first member.
if (nmhdr.code == DTN_DATETIMECHANGE)
{
// Although the DTN_DATETIMECHANGE notification struct contains the control's current date/time,
// it simplifies the code to fetch it again (performance is probably good since the control
// almost certainly just passes back a pointer to its self-maintained struct).
if (control.output_var) // Above already confirmed it has a jump_to_label (or at least an implicit cancel).
if (control.output_var && control.jump_to_label) // Set the variable's contents, for use when the corresponding TCN_SELCHANGE comes in to launch the label after this.
return 0; // 0 is appropriate for all TAB notifications.
case GUI_CONTROL_STATUSBAR:
if (!(control.jump_to_label || (control.attrib & GUI_CONTROL_ATTRIB_IMPLICIT_CANCEL)))// These is checked to avoid returning TRUE below, and also for performance.
break; // Let default proc handle it.
switch(nmhdr.code)
{
case NM_CLICK:
case NM_RCLICK:
case NM_DBLCLK:
case NM_RDBLCLK:
switch(nmhdr.code)
{
case NM_CLICK: gui_event = GUI_EVENT_NORMAL; break;
case NM_RCLICK: gui_event = GUI_EVENT_RCLK; break;
case NM_DBLCLK: gui_event = GUI_EVENT_DBLCLK; break;
case NM_RDBLCLK: gui_event = 'R'; break; // Rare, so just a simple mnemonic is stored (seems better than a digit).
}
// Pass the one-based part number that was clicked. If the user clicked near the size grip,
// apparently a large number is returned (at least on some OSes).
if (prev_color != CLR_INVALID) // Put the previous color back into effect for this DC.
SetTextColor(lpdis->hDC, prev_color);
break;
}
case WM_CONTEXTMENU:
if ((pgui = GuiType::FindGui(hWnd)) && pgui->mLabelForContextMenu)
{
HWND clicked_hwnd = (HWND)wParam;
bool from_keyboard; // Whether Context Menu was generated from keyboard (AppsKey or Shift-F10).
if ( !(from_keyboard = (lParam == 0xFFFFFFFF)) ) // Mouse click vs. keyboard event.
{
// If the click occurred above the client area, assume it was in title/menu bar or border.
// Let default proc handle it.
point_and_hwnd_type pah = {0};
pah.pt.x = LOWORD(lParam);
pah.pt.y = HIWORD(lParam);
POINT client_pt = pah.pt;
if (!ScreenToClient(hWnd, &client_pt) || client_pt.y < 0)
break; // Allows default proc to display standard system context menu for title bar.
// v1.0.38.01: Recognize clicks on pictures and text controls as occuring in that control
// (via A_GuiControl) rather than generically in the window:
if (clicked_hwnd == pgui->mHwnd)
{
// v1.0.40.01: Rather than doing "ChildWindowFromPoint(clicked_hwnd, client_pt)" -- which fails to
// detect text and picture controls (and perhaps others) when they're inside GroupBoxes and
// Tab controls -- use the MouseGetPos() method, which seems much more accurate.
EnumChildWindows(clicked_hwnd, EnumChildFindPoint, (LPARAM)&pah); // Find topmost control containing point.
clicked_hwnd = pah.hwnd_found; // Okay if NULL; the next stage will handle it.
}
}
// Finding control_index requires only GUI_HWND_TO_INDEX (not FindControl) since context menu message
// never arrives for a ComboBox's Edit control (since that control has its own context menu).
control_index = GUI_HWND_TO_INDEX(clicked_hwnd); // Yields a small negative value on failure, which due to unsigned is seen as a large positive number.
if (control_index >= pgui->mControlCount) // The user probably clicked the parent window rather than inside one of its controls.
control_index = NO_CONTROL_INDEX;
// Above flags it as a non-control event. Must use NO_CONTROL_INDEX rather than something
// like 0xFFFFFFFF so that high-order bit is preserved for use below.
// Above: NO_CONTROL_INDEX indicates to GetGuiControl() that there is no control in this case.
POST_AHK_GUI_ACTION(hWnd, control_index, GUI_EVENT_DROPFILES, NO_EVENT_INFO); // The HDROP is not passed via message so that it can be released (via the destructor) if the program closes during the drop operation.
// MsgSleep() is not done because "case AHK_GUI_ACTION" in GuiWindowProc() takes care of it.
// See its comments for why.
return 0; // "An application should return zero if it processes this message."
}
case AHK_GUI_ACTION:
case AHK_USER_MENU:
// v1.0.36.03: The g_MenuIsVisible check was added as a means to discard the message. Otherwise
// MSG_FILTER_MAX would result in a bouncing effect or something else that disrupts a popup menu,
// namely a context menu shown by an AltSubmit ListView (regardless of whether it's shown by
// GuiContextMenu or in response to a RightClick event). This is because a ListView apparently
// generates the following notifications while the context menu is displayed:
// C: release mouse capture
// H: hottrack
// f: lost focus
// I think the issue here is that there are times when messages should be reposted and
// other times when they should not be. The MonthCal and DateTime cases mentioned below are
// times when they should, because the MSG_FILTER_MAX filter is not in effect then. But when a
// script's own popup menu is displayed, any message subject to filtering (which includes
// AHK_GUI_ACTION and AHK_USER_MENU) should probably never be reposted because that disrupts
// the ability to select an item in the menu or dismiss it (possibly due to the theoretical
// bouncing-around effect described below).
// OLDER: I don't think the below is a complete explanation since it doesn't take into account
// the fact that the message filter might be in effect due to a menu being visible, which if
// true would prevent MsgSleep from processing the message.
// OLDEST: MsgSleep() is the critical step. It forces our thread msg pump to handle the message now
// because otherwise it would probably become a CPU-maxing loop wherein the dialog or MonthCal
// msg pump that called us dispatches the above message right back to us, causing it to
// bounce around thousands of times until that other msg pump finally finishes.
if (!g_MenuIsVisible)
{
// Handling these messages here by reposting them to our thread relieves the one who posted them
// from ever having to do a MsgSleep(-1), which in turn allows it or its caller to acknowledge
// its message in a timely fashion, which in turn prevents undesirable side-effects when a
// g-labeled DateTime's drop-down is navigated via its arrow buttons (jumps ahead two months
// instead of one, infinite loop with mouse button stuck down on some systems, etc.). Another
// side-effect is the failure of a g-labeled MonthCal to be able to notify of date change when
// the user clicks the year and uses the spinner to select a new year. This solves both of
FindGroup(aControlIndex, radio_start, radio_end); // Even if the return value is 1, do the below because it ensures things like tabstop are in the right state.
if (aCheckType == BST_CHECKED)
// This will check the specified button and uncheck all the others in the group.
// There is at least one other reason to call CheckRadioButton() rather than doing something
// manually: It prevents an unwanted firing of the radio's g-label upon WM_ACTIVATE,
// at least when a radio group is first in the window's z-order and the radio group has
// Caller has ensured that aControl.type is ListView.
// Caller has ensured that aOpt.color_bk is CLR_INVALID if no change should be made to the
// current background color.
{
if (aOpt.limit)
{
if (ListView_GetItemCount(aControl.hwnd) > 0)
SendMessage(aControl.hwnd, LVM_SETITEMCOUNT, aOpt.limit, 0); // Last parameter should be 0 for LVS_OWNERDATA (verified if you look at the definition of ListView_SetItemCount macro).
else
// When the control has no rows, work around the fact that LVM_SETITEMCOUNT delivers less than 20%
// of its full benefit unless done after the first row is added (at least on XP SP1). The message
focus_was_set = true; // Tell the below not to set focus, since all tab controls are hidden or disabled.
else if (aFocusFirstControl) // Note that SetFocus() has an effect even if the parent window is hidden. i.e. next time the window is shown, the control will be focused.
focus_was_set = false; // Tell it to focus the first control on the new page.
else
{
HWND focused_hwnd;
GuiControlType *focused_control;
// If the currently focused control is somewhere in this tab control (but not the tab control
// itself, because arrow-key navigation relies on tabs stay focused while the user is pressing
// left and right-arrow), override the fact that aFocusFirstControl is false so that when the
// page changes, its first control will be focused:
// Don't use IsWindowVisible() because if the parent window is hidden, I think that will
// always say that the controls are hidden too. In any case, IsWindowVisible() does not
// work correctly for this when the window is first shown:
style = GetWindowLong(control.hwnd, GWL_STYLE);
has_visible_style = style & WS_VISIBLE;
has_enabled_style = !(style & WS_DISABLED);
// Showing/hiding/enabling/disabling only when necessary might cut down on redrawing:
control_state_altered = false; // Set default.
if (will_be_visible)
{
if (!has_visible_style)
{
ShowWindow(control.hwnd, SW_SHOWNOACTIVATE);
control_state_altered = true;
}
}
else
if (has_visible_style)
{
ShowWindow(control.hwnd, SW_HIDE);
control_state_altered = true;
}
if (will_be_enabled)
{
if (!has_enabled_style)
{
EnableWindow(control.hwnd, TRUE);
control_state_altered = true;
}
}
else
if (has_enabled_style)
{
// Note that it seems to make sense to disable even text/pic/groupbox controls because
// they can receive clicks and double clicks (except GroupBox).
EnableWindow(control.hwnd, FALSE);
control_state_altered = true;
}
if (control_state_altered)
{
// If this altered control lies at least partially outside the tab's interior,
// set it up to do the full repaint of the parent window:
GetWindowRect(control.hwnd, &rect);
if (!(PtInRect(&tab_rect, rect_pt[0]) && PtInRect(&tab_rect, rect_pt[1])))
invalidate_entire_parent = true;
}
// The above's use of show/hide across a wide range of controls may be necessary to support things
// such as the dynamic removal of tabs via "GuiControl,, MyTab, |NewTabSet1|NewTabSet2", i.e. if the
// newly added removed tab was active, it's controls should now be hidden.
// The below sets focus to the first input-capable control, which seems standard for the tab-control
// dialogs I've seen.
if (!focus_was_set && member_of_current_tab && will_be_visible && will_be_enabled)
{
switch(control.type)
{
case GUI_CONTROL_TEXT:
case GUI_CONTROL_PIC:
case GUI_CONTROL_GROUPBOX:
case GUI_CONTROL_PROGRESS:
case GUI_CONTROL_MONTHCAL:
case GUI_CONTROL_UPDOWN: // It appears that not even non-buddied up-downs can be focused.
break; // Do nothing for the above types because they cannot be focused.
default:
//case GUI_CONTROL_STATUSBAR: Nothing needs to be done because other logic has ensured it can't be a member of any tab.
//case GUI_CONTROL_BUTTON:
//case GUI_CONTROL_CHECKBOX:
//case GUI_CONTROL_RADIO:
//case GUI_CONTROL_DROPDOWNLIST:
//case GUI_CONTROL_COMBOBOX:
//case GUI_CONTROL_LISTBOX:
//case GUI_CONTROL_LISTVIEW:
//case GUI_CONTROL_TREEVIEW:
//case GUI_CONTROL_EDIT:
//case GUI_CONTROL_DATETIME:
//case GUI_CONTROL_HOTKEY:
//case GUI_CONTROL_SLIDER:
//case GUI_CONTROL_TAB:
// Fix for v1.0.24: Don't check the return value of SetFocus() because sometimes it returns
// NULL even when the call will wind up succeeding. For example, if the user clicks on
// the second tab in a tab control, SetFocus() will probably return NULL because there
// is not previously focused control at the instant the call is made. This is because
// the control that had focus has likely already been hidden and thus lost focus before
// we arrived at this stage:
SetFocus(control.hwnd); // Note that this has an effect even if the parent window is hidden. i.e. next time the parent is shown, this control will be focused.
focus_was_set = true; // i.e. SetFocus() only for the FIRST control that meets the above criteria.
}
}
}
if (parent_is_visible_and_not_minimized) // Fix for v1.0.25.14. See further above for details.
SendMessage(mHwnd, WM_SETREDRAW, TRUE, 0); // Re-enable drawing before below so that tab can be focused below.
// In case tab is empty or there is no control capable of receiving focus, focus the tab itself
// instead. This allows the Ctrl-Pgdn/Pgup keyboard shortcuts to continue to navigate within
// this tab control rather than having the focus get kicked backed outside the tab control
// -- which I think happens when the tab contains no controls or only text controls (pic controls
// seem okay for some reason), i.e. if the control with focus is hidden, the dialog falls back to
// giving the focus to the the first focus-capable control in the z-order.
if (!focus_was_set)
SetFocus(aTabControl.hwnd); // Note that this has an effect even if the parent window is hidden. i.e. next time the parent is shown, this control will be focused.
// UPDATE: Below is now only done when necessary to cut down on flicker:
// Seems best to invalidate the entire client area because otherwise, if any of the tab's controls lie
// outside of its interior (this is common for TCS_BUTTONS style), they would not get repainted properly.
// In addition, tab controls tend to occupy the majority of their parent's client area anyway:
// This indicates it's not a member of a tab control. Callers rely on this check.
return NULL;
TabControlIndexType tab_control_index = 0;
for (GuiIndexType u = 0; u < mControlCount; ++u)
if (mControl[u].type == GUI_CONTROL_TAB)
if (tab_control_index == aTabControlIndex)
return &mControl[u];
else
++tab_control_index;
return NULL; // Since above didn't return, indicate failure.
}
int GuiType::FindTabIndexByName(GuiControlType &aTabControl, char *aName, bool aExactMatch)
// Find the first tab in this tab control whose leading-part-of-name matches aName.
// Return int vs. TabIndexType so that failure can be indicated.
{
int tab_count = TabCtrl_GetItemCount(aTabControl.hwnd);
if (!tab_count)
return -1; // No match.
if (!*aName)
return 0; // First item (index 0) matches the empty string.
TCITEM tci;
tci.mask = TCIF_TEXT;
char buf[1024];
tci.pszText = buf;
tci.cchTextMax = sizeof(buf) - 1; // MSDN example uses -1.
size_t aName_length = strlen(aName);
if (aName_length >= sizeof(buf)) // Checking this early avoids having to check it in the loop.
return -1; // No match possible.
for (int i = 0; i < tab_count; ++i)
{
if (TabCtrl_GetItem(aTabControl.hwnd, i, &tci))
{
if (aExactMatch)
{
if (!strcmp(tci.pszText, aName)) // Match found.
return i;
}
else
{
tci.pszText[aName_length] = '\0'; // Facilitates checking of only the leading part like strncmp(). Buffer overflow is impossible due to a check higher above.
if (!lstrcmpi(tci.pszText, aName)) // Match found.
return i;
}
}
}
// Since above didn't return, no match found.
return -1;
}
int GuiType::GetControlCountOnTabPage(TabControlIndexType aTabControlIndex, TabIndexType aTabIndex)
{
int count = 0;
for (GuiIndexType u = 0; u < mControlCount; ++u)
if (mControl[u].tab_index == aTabIndex && mControl[u].tab_control_index == aTabControlIndex) // This boolean order helps performance.
++count;
return count;
}
POINT GuiType::GetPositionOfTabClientArea(GuiControlType &aTabControl)
// Gets position of tab control relative to parent window's client area.
{
RECT rect, entire_rect;
GetWindowRect(aTabControl.hwnd, &entire_rect);
POINT pt = {entire_rect.left, entire_rect.top};
ScreenToClient(mHwnd, &pt);
GetClientRect(aTabControl.hwnd, &rect); // Used because the coordinates of its upper-left corner are (0,0).
void GuiType::ControlGetPosOfFocusedItem(GuiControlType &aControl, POINT &aPoint)
// Caller has ensured that aControl is the focused control if the window has one. If not,
// aControl can be any other control.
// Based on the control type, the position of the focused subitem within the control is
// returned (in screen coords) If the control has no focused item, the position of the
// control's caret (which seems to work okay on all control types, even pictures) is returned.
{
LRESULT index;
RECT rect;
rect.left = COORD_UNSPECIFIED; // Init to detect whether rect has been set yet.
switch (aControl.type)
{
case GUI_CONTROL_LISTBOX: // Testing shows that GetCaret() doesn't report focused row's position.
index = SendMessage(aControl.hwnd, LB_GETCARETINDEX, 0, 0); // Testing shows that only one item at a time can have focus, even when mulitple items are selected.
// If above didn't get the rect for either reason, a default method is used later below.
break;
case GUI_CONTROL_LISTVIEW: // Testing shows that GetCaret() doesn't report focused row's position.
index = ListView_GetNextItem(aControl.hwnd, -1, LVNI_FOCUSED); // Testing shows that only one item at a time can have focus, even when mulitple items are selected.
if (index != -1)
{
// If the focused item happens to be beneath the viewable area, the context menu gets
// displayed beneath the ListView, but this behavior seems okay because of the rarity
// and because Windows Explorer behaves the same way.
// Don't use the ListView_GetItemRect macro in this case (to cut down on its code size).
rect.left = LVIR_LABEL; // Seems better than LVIR_ICON in case icon is on right vs. left side of item.
//else a default method is used later below, flagged by rect.left==COORD_UNSPECIFIED.
break;
case GUI_CONTROL_TREEVIEW: // Testing shows that GetCaret() doesn't report focused row's position.
HTREEITEM hitem;
if (hitem = TreeView_GetSelection(aControl.hwnd)) // Same as SendMessage(aControl.hwnd, TVM_GETNEXTITEM, TVGN_CARET, NULL).
// If the focused item happens to be beneath the viewable area, the context menu gets
// displayed beneath the ListView, but this behavior seems okay because of the rarity
// and because Windows Explorer behaves the same way.
// Don't use the ListView_GetItemRect macro in this case (to cut down on its code size).
TreeView_GetItemRect(aControl.hwnd, hitem, &rect, TRUE); // Pass TRUE because caller typically wants to display a context menu, and this gives a more precise location for it.
//else a default method is used later below, flagged by rect.left==COORD_UNSPECIFIED.
break;
case GUI_CONTROL_SLIDER: // GetCaretPos() doesn't retrieve thumb position, so it seems best to do so in case slider is very tall or long.
SendMessage(aControl.hwnd, TBM_GETTHUMBRECT, 0, (WPARAM)&rect); // No return value.
break;
}
// Notes about control types not handled above:
//case GUI_CONTROL_STATUSBAR: For this and many others below, caller should never call it for this type.
//case GUI_CONTROL_TEXT:
//case GUI_CONTROL_PIC:
//case GUI_CONTROL_GROUPBOX:
//case GUI_CONTROL_BUTTON:
//case GUI_CONTROL_CHECKBOX:
//case GUI_CONTROL_RADIO:
//case GUI_CONTROL_DROPDOWNLIST:
//case GUI_CONTROL_COMBOBOX:
//case GUI_CONTROL_EDIT: Has it's own context menu.
//case GUI_CONTROL_DATETIME:
//case GUI_CONTROL_MONTHCAL: Has it's own context menu. Can't be focused anyway.
//case GUI_CONTROL_HOTKEY:
//case GUI_CONTROL_UPDOWN:
//case GUI_CONTROL_PROGRESS:
//case GUI_CONTROL_TAB: For simplicity, just do basic reporting rather than trying to find pos. of focused tab.
if (rect.left == COORD_UNSPECIFIED) // Control's rect hasn't yet been fetched, so fall back to default method.
{
GetWindowRect(aControl.hwnd, &rect);
// Decided againt this since it doesn't seem to matter for any current control types. If a custom
// context menu is ever supported for Edit controls, maybe use GetCaretPos() for them.
//GetCaretPos(&aPoint); // For some control types, this might give a more precise/appropriate position than GetWindowRect().
//ClientToScreen(aControl.hwnd, &aPoint);
//// A little nicer for most control types (such as DateTime) to shift it down a little so that popup/context
//// menu or tooltip doesn't fully obstruct the control's contents.
//aPoint.y += 10; // A constant 10 is used because varying it by font doesn't seem worthwhile given that menu/tooltip fonts are of fixed size.
}
else
MapWindowPoints(aControl.hwnd, NULL, (LPPOINT)&rect, 2); // Convert rect from client coords to screen coords.
aPoint.x = rect.left;
aPoint.y = rect.top + 2 + (rect.bottom - rect.top)/2; // +2 to shift it down a tad, revealing more of the selected item.
// Above: Moving it down a little by default seems desirable 95% of the time to prevent it
// from covering up the focused row, the slider's thumb, a datetime's single row, etc.
}
struct LV_SortType
{
LVFINDINFO lvfi;
LVITEM lvi;
HWND hwnd;
lv_col_type col;
char buf1[LV_TEXT_BUF_SIZE];
char buf2[LV_TEXT_BUF_SIZE];
bool sort_ascending;
bool incoming_is_index;
};
int CALLBACK LV_GeneralSort(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort)
// ListView sorting by field's text or something derived from the text for each call.
{
LV_SortType &lvs = *(LV_SortType *)lParamSort;
// v1.0.44.12: Testing shows that LVM_GETITEMW automatically converts the ANSI contents of our ListView
// into Unicode, which is nice because it avoids the overhead and code size of having to call
// ToWideChar(), along with the extra/temp buffers it requires to receive the wide version.
? LVM_GETITEMW : LVM_GETITEM; // Both items above are checked so that SCS_INSENSITIVE_LOGICAL can be effect even for non-text columns because it allows a column to be later changed to TEXT and retain its "logical-sort" setting.
// NOTE: It's safe to send a LVITEM struct rather than an LVITEMW with the LVM_GETITEMW message because
// the only difference between them is the type "LPWSTR pszText", which is no problem as long as caller
// has properly halved cchTextMax to reflect that wide-chars are twice as wide as 8-bit characters.
// MSDN: "During the sorting process, the list-view contents are unstable. If the [ListView_SortItems]
// callback function sends any messages to the list-view control, the results are unpredictable (aside
// from LVM_GETITEM, which is allowed by ListView_SortItemsEx but not ListView_SortItems)."
// Since SortItemsEx has become so much more common/available, the doubt about whether the non-Ex
// ListView_SortItems actually allows LVM_GETITEM (which it probably does in spite of not being
// documented) much less of a concern.
// Older: It seems hard to believe that you shouldn't send ANY kind of message because how could you
// ever use ListView_SortItems() without either having LVS_OWNERDATA or allocating temp memory for the
// entire column (to whose rows lParam would point)?
// UPDATE: The following seems to be one alternative:
// Do a "virtual qsort" on this column's contents by having qsort() sort an array of row numbers according
// to the contents of each particular row's field in that column (i.e. qsort's callback would call LV_GETITEM).
// In other words, the array would start off in order (1,2,3) but afterward would contain the proper sort
// (e.g. 3,1,2). Next, traverse the array and store the correct "order number" in the corresponding row's
// special "lParam container" (for example, 3,1,2 would store 1 in row 3, 2 in row 1, and 3 in row 2, and 4 in...).
// Then the ListView can be sorted via a method like the high performance LV_Int32Sort.
// However, since the above would require TWO SORTS, it would probably be slower (though the second sort would
// require only a tiny fraction of the time of the first).
lvs.lvi.pszText = lvs.buf1; // lvi's other members were already set by the caller.
if (lvs.incoming_is_index) // Serves to avoid the potentially high performance overhead of ListView_FindItem() where possible.
{
lvs.lvi.iItem = (int)lParam1;
SendMessage(lvs.hwnd, msg_lvm_getitem, 0, (LPARAM)&lvs.lvi); // Use LVM_GETITEM vs. LVM_GETITEMTEXT because MSDN says that only LVM_GETITEM is safe during the sort.
}
else
{
// Unfortunately, lParam cannot be used as the index itself because apparently, the sorting
// process puts the item indices into a state of flux. In other words, the indices are
// changing while the sort progresses, so it's not possible to use an item's original index
// Must use lvi.pszText vs. buf because MSDN says (for LVM_GETITEM, but it might also apply to
// LVM_GETITEMTEXT even though it isn't documented): "Applications should not assume that the text will
// necessarily be placed in the specified buffer. The control may instead change the pszText member
// of the structure to point to the new text rather than place it in the buffer."
char *field1 = lvs.lvi.pszText; // Save value of pszText in case it no longer points to lvs.buf1.
// Fetch Item #2 (see comments in #1 above):
lvs.lvi.pszText = lvs.buf2; // lvi's other members were already set by the caller.
if (lvs.incoming_is_index)
{
lvs.lvi.iItem = (int)lParam2;
SendMessage(lvs.hwnd, msg_lvm_getitem, 0, (LPARAM)&lvs.lvi); // Use LVM_GETITEM vs. LVM_GETITEMTEXT because MSDN says that only LVM_GETITEM is safe during the sort.
}
else
{
// Set any lvfi members not already set by the caller. Note that lvi.mask was set to LVIF_TEXT by the caller.
// Init those members needed for LVM_GETITEM if it turns out to be needed. This section
// also serves to permanently init cchTextMax for use by the sorting functions too:
lvs.lvi.pszText = lvs.buf1;
lvs.lvi.cchTextMax = LV_TEXT_BUF_SIZE - 1; // Set default. Subtracts 1 because of that nagging doubt about size vs. length. Some MSDN examples subtract one, such as TabCtrl_GetItem()'s cchTextMax.
if (col.type == LV_COL_INTEGER)
{
// Testing indicates that the following approach is 25 times faster than the general-sort method.
// Assign the 32-bit integer as the items lParam at this early stage rather than getting the text
// and converting it to an integer for every call of the sort proc.
? ATOI(lvs.lvi.pszText) : 0; // Must not refer to lvs.buf1 directly because MSDN says LVM_GETITEMTEXT might have changed pszText to point to some other string.
lvs.lvi.mask = LVIF_PARAM;
lvs.lvi.iSubItem = 0; // Indicate that an item vs. subitem is being operated on (subitems can't have an lParam).
ListView_SetItem(aControl.hwnd, &lvs.lvi);
}
// Always use non-Ex() for this one because it's likely to perform best due to the lParam setup above.
// The value of iSubItem is not reset to aColumnIndex because LV_Int32Sort() doesn't use it.
SendMessage(aControl.hwnd, LVM_SORTITEMS, lvs.sort_ascending, (LPARAM)LV_Int32Sort); // Should always succeed since it uses non-Ex() version.
}
else // It's LV_COL_TEXT or LV_COL_FLOAT.
{
if (col.type == LV_COL_TEXT && col.case_sensitive == SCS_INSENSITIVE_LOGICAL) // SCS_INSENSITIVE_LOGICAL can be in effect even when type isn't LV_COL_TEXT because it allows a column to be later changed to TEXT and retain its "logical-sort" setting.
{
// v1.0.44.12: Support logical sorting, which treats numeric strings as true numbers like Windows XP
// Explorer's sorting. This is done here rather than in LV_ModifyCol() because it seems more
// maintainable/robust (plus LV_GeneralSort() relies on us to do this check).
if (!g_StrCmpLogicalW)
{
HINSTANCE hinstLib;
if (hinstLib = LoadLibrary("shlwapi")) // For code simplicity and performance-upon-reuse, once loaded it is never freed.
if (g_StrCmpLogicalW) // Generally, this happens only if OS is older than XP. But OS version isn't checked in case it's possible for older OSes/emultators to ever have StrCmpLogicalW().
lvs.lvi.cchTextMax = lvs.lvi.cchTextMax/2 - 1; // Buffer can hold only half as many Unicode characters as non-Unicode (subtract 1 for the extra-wide NULL terminator).
else
col.case_sensitive = SCS_INSENSITIVE_LOCALE; // LV_GeneralSort() relies on this fallback. Also, it falls back to the LOCALE method because it is the closest match to LOGICAL (since testing shows that StrCmpLogicalW seems to use the user's locale).
}
// Since LVM_SORTITEMSEX requires comctl32.dll version 5.80+, the non-Ex version is used
// whenever the EX version fails to work. One reason to strongly prefer the Ex version
// is that MSDN says the non-Ex version shouldn't query the control during the sort,
// which although hard to believe, is a concern. Therefore:
// Try to use the SortEx() method first. If it doesn't work, fall back to the non-Ex method under
// the assumption that the OS doesn't support the Ex() method.
// Initialize struct members as much as possible so that the sort callback function doesn't have to do it
// the many times it's called. Some of the others were already initialized higher above for internal use here.
lvs.hwnd = aControl.hwnd;
lvs.lvi.iSubItem = aColumnIndex; // Zero-based column index to indicate whether the item or one of its sub-items should be retrieved.
lvs.col = col; // Struct copy, which should enhance sorting performance over a pointer.
lvs.incoming_is_index = true;
lvs.lvi.pszText = NULL; // Serves to detect whether the sort-proc actually ran (it won't if this is Win95 or some other OS that lacks SortEx).